The goal of the Miraj Project is to create a pure Clojure, 100% functional programming model for web application development, including first-class support for defining and using Web Components (Polymer only for this version).
Miraj is largely motivated by the following observations:
- The three languages of the web (HTML, Javascript, CSS) together serve as a kind of web "assembly" language. Nobody wants to program in assembly, let alone three different assembly languages.
- HTML element tags are (co-)functions. Like functions, they are applied to arguments (attributes and child elements), and they always do the same thing given the same input (and evaluation environment). The only difference is that functions result in values, and HTML element co-functions result in behavioral side-effects.
- An HTML page is analogous to a Clojure program of one function, main. In particular, the
and<link>
elements in the<script>
element are analogous to the<head>
and:require
directives of Clojure's:import
macro: they tell the runtime to find, fetch, and load the referenced resources.ns
Miraj eliminates mixed-language programming, allowing the programmer to define pages and components in Clojure. Miraj compiles this Clojure code into HTML, Javascript, and CSS.
Clojurescript already eliminates the need to program in Javascript; Miraj does the same for HTML. (A genuine Clojure face for CSS programming remains a future project).
Providing Clojure functions for HTML elements is
relatively trivial. Miraj also provides macros that
make page and component definitions look similar to
the deftype
defrecord
Things get a little more complicated when you add web components. Miraj allows the programmer to define and use Polymer-based components in idiomatic Clojure, without having to worry about the directory structures, file names, and href values required to make the generated HTML, Javascript and CSS files work together.
Miraj also makes it very easy to define and
share component libraries. Multiple components may be
defined across multiple namespaces; a deflibrary
Components can also be easily defined as one-off elements for use in a single page. Both page and components can be defined in the same project.
Miraj is composed of several layers:
- The base layer is miraj.co-dom. This library
is derived from version 0.8.0 of data.xml. Miraj uses miraj.co-dom to build an XML tree
representation of the HTML, which it then
serializes to an HTML5 string. For example,
is represented by<span>Hello, world!</span>
. Normally, the programmer will not need to use the co-dom library directly.#miraj.co_dom.Element{:tag :span, :attrs {}, :content ("Hello, world!")}
- The miraj.html library
is a wrapper around
miraj.co-dom
; it provides one Clojure function per HTML5 element tag. For example, in a repl: This serializes toindex> (def howdy (h/span "Hello, world!")) #'index/howdy index> howdy #miraj.co_dom.Element{:tag :span, :attrs {}, :content ("Hello, world!")}
.<span>Hello, world!</span>
- The miraj.core library
adds support for:
- A
macro for defining HTML5 pages.defpage
- A
macro for defining (Polymer) web components.defcomponent
- A
macro for defining component libraries.deflibrary
- Functions for compiling and linking (i.e. serializing) Miraj structures.
- A
- A collection of Polymer libraries provides support for Polymer components.
- boot-miraj, a boot task library for Miraj programming.
NOTE: this website was build using Miraj. The source code is available at:miraj-project/homepage. Many other simple examples with commented code are available at miraj-project/demos/hello-world
HTML5
The miraj.html library wraps the lower-level miraj.co-dom library, providing one function per HTML5 element, as well as some additional goodies.
Assuming miraj.html
h
Elements
- One function per HTML element:
(h/div (h/span "Hello World"))
<div><span>Hello World</span></div>
- HTML5 void elements cannot have any content; they also cannot be "self-closing"; they may only have a start tag with no '/'. Miraj understands void elements:
(h/br)
<br>
- HTML5 empty elements must not be self-closing; they must have a close tag. Miraj understands empty elements:
(h/script {:src "foo.js"})
<script src="foo.js"></script>
- It's all functions; compose at will:
(let [stooges ["Larry" "Moe" "Curly"]] (h/ul (for [stooge stooges] (h/li stooge))))
<ul><li>Larry</li> <li>Moe</li> <li>Curly</li></ul>
- Larry
- Moe
- Curly
Text Nodes
- Escaping of <angle brackets> & ampersands is handled automatically:
(h/span "Hello & Goodbye, <i>World</i>")
<span>Hello & Goodbye, <i>World</i></span>
Displays as:Hello & Goodbye, <i>World</i>
- Embedded double quotes must be escaped:
(h/span "Hello \"World\" ('Howdy')")
<span>Hello "World" ('Howdy')</span>
- Character entity references like € require special handling, since & is automatically escaped. Use the Unicode literal (for example, \u20AC for the Euro sign, €). You can embed character literals directly, or you can use ordinary Clojure definitions or bindings to get names:
(let [euro-sign "\u20AC"] (h/span "Hello, Euro: " euro-sign))
<span>Hello, Euro: €</span>
Displays as:Hello, Euro: €
- WARNING: if you are using Polymer, you must escape opening double braces
{{
and brackets[[
if you want to display them in a string, since Polymer treats these as special "binding annotations" (see polymer for more info). I.e. if you put something inside double braces or brackets, it will be interpreted as a property and will be displayed as null if it has no value: displays as:(h/span "Hello {{World}}")
<span>Hello {{World}}</span>
Hello {{World}}
Use the Unicode character \uFEFF, 'ZERO WIDTH NO-BREAK SPACE', to force display of the delimiters without Polymer interpretation: displays as:(h/span "Hello {\uFEFF{World}}")
<span>Hello {\uFEFF{World}}</span>
Hello {{World}}
- You can split text nodes:
(h/span "Lorem ipsum dolor sit amet," " consectetur adipiscing elit.")
<span>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</span>
- You can use Clojure expressions:
(h/span "Lorem ipsum" (+ 2 3))
<span>Lorem ipsum 6</span>
(h/span "Lorem " (clojure.string/join ", " (repeat 3 "ipsum")))
<span>Lorem ipsum, ipsum, ipsum</span>
- Inline elements are easy!
(h/span "Inline elemeents are " (h/i "easy") "!")
<span>Inline elements are <i>easy</i>!</span>
Attributes
- Attributes are passed as a keyword map:
(h/span {:class "foo"} "Hello World")
<span class="foo">Hello World</span>
- With a few exceptions, clojure attribute values go through normal Clojure evaluation and then are serialized as strings. You can use expressions as attribute values:
(h/span {:foo (* 2 3)})
<span foo="6"></span>
- BigInt and BigDecimal end up looking like Int and Decimal:
(h/span {:foo 9N})
<span foo="9"></span>
See miraj.co-dom for more examples.
Sugar
For id, class, and boolean attributes:
(h/span :#foo "Hello World")
<span id="foo">Hello World</span>
(h/span :.bar.baz "Hello World")
<span class="bar baz">Hello World</span>
(h/span :?centered?horizontal "Hello World")
<span centered horizontal>Hello World</span>
(h/span :#foo.bar?centered "Hello World")
<span id="foo" class="bar" centered>Hello World</span>
For inline styles, use the miraj.style
(h/span {:miraj.style/color "blue"} "Hello World")
<span style="color:blue;">Hello World</span>
Pseudo-classes and -elements are supported:
- Hover: Hello World
(h/span :#foo {:miraj.style/hover {:color "red"} "Hello World")
<span id="foo">Hello World</span><style>#foo:hover {color:red;}</style>
- using :before pseudo element: World
(h/span :#foo {:miraj.style/before {:content "Howdy, "} "World")
<span id="foo">World</span><style>#foo:before {content: "Howdy, "}</style>
HTML Metadata
Pass HTML metadata as a Clojure map; Miraj will validate the map against Clojure.spec specifications (which may be found in miraj_spec.clj and miraj/x/, and then transform it into the appropriate elements in <head>.
#::h{:title "Page Title" :description "Page description" :charset "utf-8" :viewport {::h/width :device-width ::h/scale {::h/min 0.5 ::h/max 2 ::h/initial 1} ::h/user-scalable true}}
<meta content="utf-8" name="charset"> <title>Page Title</title> <meta content="Page description" name="description"> <meta content="width=device-width, minimum-scale=0.5, maximum-scale=2, initial-scale=1, user-scalable=yes" name="viewport">
Miraj
Pages
See the Hello World demos for many examples.
To define an index.html page,
use miraj.core/defpage
(ns index (:require [miraj.core :as miraj :refer [defpage]] [miraj.html :as h])) (defpage "PAGE DOCSTRING" #::h{:title "Page Title" :description "Page description" :charset "utf-8" :viewport {::h/width :device-width ::h/scale {::h/min 0.5 ::h/max 2 ::h/initial 1} ::h/user-scalable true}} (:body :?unresolved (h/h1 "Hello, World Demo")))
<!doctype html> <!-- generated index.html --> <html> <head> <meta content="utf-8" name="charset"> <title>Page Title</title> <meta content="Page description" name="description"> <meta content="width=device-width, minimum-scale=0.5, maximum-scale=2, initial-scale=1, user-scalable=yes" name="viewport"> <link rel="import" href="/miraj/polymer/assets/paper-card/paper-card.html"> </head> <body unresolved> <h1>Hello, World Demo</h1> </body> </html>
Polymer
Miraj provides a library, miraj.polymer, that supports features specific to Polymer. For example, it supports Polymer bindings, helper elements, and Event protocols.
Polymer Component Libraries
In addition, Miraj provides a collection of pre-built libraries for the collection of components built by the Polymer Project. These libraries wrap the native Polymer implementations, which can be found at webcomponents.org
Warning: only the iron and paper libraries are fully up to date; the remaining libraries are outdated, but will soon be upgraded.
- miraj.polymer.iron "Basic building blocks for creating an application." (iron-elements)
- miraj.polymer.paper Material design UI elements. (paper-elements)
- miraj.polymer.gold "Elements built for e-commerce-specific use-cases, like checkout flows." (gold-elements)
- miraj.polymer.google
- miraj.polymer.layout
- miraj.polymer.molecules
- miraj.polymer.neon
- miraj.polymer.platinum
Polymer Assets
The assets that implement Polymer components are package in miraj.polymer.assets
; this library contains everything you would
get if you installed using bower, packaged as a
jarfile so the assets become available. via the
classpath. Each of the miraj.polymer.*
libraries
has a dependency on this library, so the user
never needs to import it directly.
To serve your component-based application
statically, or using a non-Java server, you must copy the assets your
app needs to a folder on the server's search path.
The boot-miraj/assetize
task will copy the contents of
the miraj.polymer.assets jar to the filesystem. Alternatively, you
can use bower to install the components you need, but the path to them
must be miraj/polymer/assets.
Using Polymer Components
To use a Polymer component in a webpage,
include the library as a dependency in your
boot/leiningen project file, and
then :require
it in your Clojure
namespace, just like any other
library: (ns foo.bar ...) (defpage baz (:require [miraj.polymer.paper :as paper :refer [button card]]) ...)
See the Polymer hello-world demo for more detailed examples.
Miraj generally follows a simple naming
convention for Polymer components: <foo-bar> becomes
miraj.polymer.foo/bar. For example, paper-button maps to
miraj.polymer.paper/button. In some cases, another ns segment is used; for example,
the function for <paper-input-container>
is miraj.polymer.paper.input/container
. (Documentation is
incomplete, but the library source code is easily understandable.)
Data Binding Helper Elements
Polymer's data
binding helper elements –<dom-if>,
<dom-repeat>, etc. – are implemented
in miraj.polymer
. Some of the names
have been changed to be more consistent with Clojure
practice; for example, for <array-selector> we use
miraj.polymer/selection. See the source code for the complete list.
Polymer Bindings
Polymer implements a data binding mechanism that "[C]onnects data from a custom element (the host element) to a property or attribute of an element in its local DOM (the child or target element)." Miraj provides idiomatic Clojure support for Polymer bindings.
Polymer "binding annotations" look like this:
- One-way property bindings use double square brackets:
<my-element my-property="[[hostProperty]]">
- One-way attribute bindings suffix $ to the attribute name:
<my-element my-attribute$="[[hostProperty]]">
- Two-way property bindings use double squiggle braces:
<my-element my-property="{{hostProperty}}">
- Two-way attribute bindings suffix $ to the attribute name:
<my-element my-attribute$="{{hostProperty}}">
Miraj provides functions for Polymer bindings:
(h/span {:foo (miraj.polymer/bind! :bar)}
<span foo="[[bar]]"></span>
(h/span {:foo (miraj.polymer/bind-attr! :bar)}
<span foo$="[[bar]]"></span>
(h/span {:foo (miraj.polymer/bind!! :bar)}
<span foo="{{bar}}"></span>
(h/span {:foo (miraj.polymer/bind-attr!! :bar)}
<span foo$="{{bar}}"></span>
Bindings also work in text nodes, and may be concatentated to text:
(h/span (miraj.polymer/bind! :bar))
<span>[[bar]]</span>
(h/span (str "Hello, " (miraj.polymer/bind! :name)))
<span>Hello, [[name]]</span>
(h/span {:foo (str (miraj.polymer/bind! :baseUrl) "users/bob")}
<span foo="[[baseUrl]]users/bob"></span>
Two-way binding to a non-Polymer element: Polymer uses the following special syntax: target-prop="{{hostProp::target-change-event}}"
(h/input {:value (miraj.polymer/bind!! :input->hostValue)})
<input value="{{hostValue::input}}">
Custom Components
Polymer custom components typically combine the following in a single HTML file:
- A blob of HTML to define the component's local DOM
- A dash of CSS to style the component
- Some Javascript to define the properties and methods of the component, and to register it at runtime
- A collection of <link> elements to import external dependencies
Note that all of this gets imported (loaded) in one stroke, so the component will be registered as soon as the HTML file is loaded. But this is optional; the HTML and CSS can be loaded separately from the Javascript that registers the component. This is the strategy Miraj adopts. The Miraj source code defining a component is divided as follows:
- A Clojure file contains the defcomponent definition of the component's DOM, properties, and methods
- A Clojurescript delegate namespaces contains functional implementations of the component's methods
- The CSS for the component goes in a CSS file named according to the component
- An EDN file specifies external dependencies
These files must follow a strict naming
convention (this restriction may be relaxed in a future
release): the namespaace of the component is converted
into a path by interpreting each segment of the
namespace as a directory, and the name of the
component, suffixed by the appropriate extension, is
interpreted as the filename. For example, the definition
of component foo.bar/sweet
will use the following files:
foo
├── bar.clj ;; contains defcomponent sweet
├── bar ;; organizes the optional implementation files
│ ├── sweet.cljs ;; delegate namespace
│ ├── sweet.css ;; styling specific to this component
│ └── sweet.edn ;; external resources
Compiling and linking this component results in the following output:
foo
├── bar
│ ├── sweet
│ │ └── core.cljs ;; generated code: registers component, delegates method calls to bar.sweet
│ ├── sweet.cljs ;; delegate ns
│ ├── sweet.html ;; templated local DOM plus CSS
To use this component, a web page must import sweet.html (via <link rel="import"...>), compile the Clojurescript (along with any other clojurescript used by the page) to main.js, and load the resulting Javascript file. Miraj handles all this automatically, so long as you follow the conventions.
defcomponent
Themiraj.core/defcomponent
(defcomponent sweet :html acme-sweet ;; html tag can be anything, must have at least one dash '-'
"clojure docstring"
(:require ... just like :require for clojure.core/ns ...)
(:codom ... html code here ...)
{:polymer/properties ... } ;; polymer prototype definition as clojure map
;; callbacks, modeled by protocols:
miraj.polymer.protocols/Lifecycle
(created [] (sweet/created)) ;; delegate to delegate ns
miraj.html.protocols/Mouse
(click [e] (this-as this (sweet/click this e))))
The prototype definition map is a little complex. Here is an example; see the hello-world demos for more. Polymer documentation at Declared properties and Instance methods.
;; properties defined in :polymer/properties are exposed as part of the
;; public interface of the component
{:polymer/properties {:greeting ^String{:value "hello"
:type String
:observer (fn [new old] (sweet/observe-greeting new old))}}
;; static html attributes on host (Polymer hostAttributes property)
;; these will cause html attr vals to be set at create time
;; see https://www.polymer-project.org/1.0/docs/devguide/registering-elements#host-attributes
:polymer/static {:string-attr1 "attr1"
:boolean-attr2 true
:foo "Hello"
:tabindex 0}
;; complex observers take (keyword) properties as params
;; this will be fired whenever either property changes
;; https://www.polymer-project.org/1.0/docs/devguide/observers#complex-observers
:greeting-flavor-observer (fn [:greeting :flavor]
;; delegate, passing the args as syms
(sweet/greeting-flavor-observer greeting flavor))
;; local properties - we can put them in the prototype, or in a cljs namespace
;; for polymer data binding, properties must be public (defined in :polymer/properties)
:name {:last "Smith"
:first "John"}
;; "instance" methods (https://www.polymer-project.org/1.0/docs/devguide/instance-methods)
;; with javascript, instance methods go in the component's prototype
;; with clojurescript, we don't need this - just use functions in the delegate namespace
:foo-bar (fn [evt] (this-as this (sweet/foo-bar this evt)))
}
See the acme-widgets demo for detailed examples of defining custom components.
Component Libraries
See the acme-widgets demo for a detailed example of defining, compiling, and linking a custom component library.
You can also wrap 3rd party components into a
library, just as Miraj does for Polymer Project
components. The easiest way to proceed is to copy
one of the Miraj Polymer libraries and edit.
Components are described in edn/webcomponents.edn.
Run (boot-miraj/compile :libraries true)
Dom
See Local DOM Basics and API. Miraj's defcomponent
Styling
See Styling local DOM.
To add styling to a component you have several options.
The recommended method is to create a css file named according to the component name. For example, if your component is acme.sweetness/sweeter, then your CSS file should be acme/sweetness/sweeter.css. Miraj will inject the CSS into the component definition. Se the acme-widgets demo for examples.
If you need to import external stylesheets, you can use the :css directive of defcomponent, or you can use an edn file, again named according to the component name - e.g. acme/sweetness/sweeter.edn. See hello_world/miraj/sweet.edn for details on the syntax.
boot-miraj
boot-miraj is a boot task collection for Miraj development. In general you only need two tasks, compile and link. See the hello-world demos for many examples.
To compile and link a library of components:
(deftask lib []
(comp
(miraj/compile :components #{} ;; compile all namespaces
;; :components #{'foo.bar} ;; just this one ns
;; :components #{'foo.bar/baz} ;; just this one component
:keep true ;; keep (or discard) intermediate files
:debug true) ;; log msgs, pretty printed output,
(miraj/link :libraries #{'acme/widgets} ;; a deflibrary var
:debug true)))
To compile and link a page:
(deftask app
(comp
(miraj/compile :pages #{'index}
:polyfill :lite ;; inject a Polymer polyfill
:debug true)
(miraj/link :pages #{'index}
:debug true
)))
Workflow
This is the workflow section
Status & Roadmap
This is the roadmap section