Build web components in Clojure/Script (the easy way).
Experimental. Major breaking changes unlikely, but possible.
(ns increment-counter
(:require
[subzero.core :as sz]
[subzero.plugins.component-registry :as reg]
[subzero.plugins.web-components :as wc]))
(defn on-click
[event]
(let [increment-button (.-host (.-currentTarget event))
clicks (js/parseInt (.-clicks increment-button))]
(set! (.-clicks increment-button) (inc clicks))))
(defn button-view
[{:keys [clicks]}]
[:root> {:#on {:click on-click}}
[:button (str "Clicked " clicks " times")]])
(defonce !db
(doto (sz/create-db)
(reg/install!)
(wc/install! js/document js/customElements)))
(reg/reg-component !db :incrementing-button
:view button-view
:props #{:clicks})
<incrementing-button clicks="0"></incrementing-button>
(ns identity-card
(:require
[subzero.core :as sz]
[subzero.plugins.component-registry :as reg]
[subzero.plugins.html :as html]))
(defn identity-card-view
[{:keys [name age sex]}]
[:root>
[:table
[:tbody
[:tr
[:th "Name"]
[:td name]]
[:tr
[:th "Age"]
[:td age]]
[:tr
[:th "Sex"]
[:td sex]]]]])
(defonce !db
(doto (sz/create-db)
(reg/install!)
(html/install!)))
(reg/reg-component !db :identity-card
:view identity-card-view
:props #{:name :age :sex})
(print
(html/html !db {:doctype "html"}
[:identity-card
:name "John Doe"
:age 46
:sex "Male"]))
<!DOCTYPE html>
<html>
<body>
<identity-card name="John Doe" age="46" sex="Male">
<template shadowrootmode="open">
<table>
<tbody>
<tr>
<th>Name</th>
<td>John Doe</td>
</tr>
<tr>
<th>Age</th>
<td>46</td>
</tr>
<tr>
<th>Sex</th>
<td>Male</td>
</tr>
</tbody>
</table>
</template>
</identity-card>
</body>
</html>
:view
A function which takes a map of prop values, and yields the markup to be used in rendering the component.
:props
Either a set or a map specifying the props to be provided to the :view
function, and how the library should source them.
If given as a set of keywords, these are taken as the prop names; with a value
of :default
being implied for each. For simple components, this is the most
concise, and often the correct, way to specify the component's props. See below
for the behavior of :default
props.
If given as a map, the key for each entry is taken as the prop name. This is the
key that'll be used in the map of prop values passed to the :view
function.
The value of this map determines where the prop value comes from, and can be one
of the following:
:field
- SubZero will generate a matching JavaScript property for this prop.
The name of this property will be a cammelCase version of the prop name. The
value of the property reflects that of the prop, updating the property updates
the prop.:attr
- The prop value will be sourced from element attributes with the same
name as the prop. Attribute writers and readers can be registered to
customize how the library serializes values as attributes, or parses attribute
strings back into useful values.:default
- A combination of :field
and :attr
. The current prop value
will be the last updated of either the JavaScript property, or the element
attribute matching the prop name.{:state-factory the-function}
. See below.IWatchable
- Equivalent to {:state-factory (constantly <the-watchable>)}
. See below.{:state-factory factory-fn :state-cleanup ?cleanup-fn :field ?field-name}
-
The factory-fn
will be called to produce an IWatchable
, which SubZero will
watch for new prop values. If the returned value also satisfies IDeref
,
it'll be deref'd for an initial prop value. An optional cleanup-fn
can be
provided, which will be called to perform any cleanup when a component instances
is disconnected from the DOM. The optional :field
option can provide a name
for a read-only JavaScript property whose value will reflect that of this
prop.{:attr ?attr-name :field ?field-name}
- Similar to :field
, :attr
, and
:default
, except the field and attribute names are given explicitly.:focus
This determines how the component should handle focus. It can be specified as
either :self
or :delegate
. If not specified, the component will not be
focusable.
The :self
option indicates that the component itself serves as some kind of
control, and should thus be focusable. SubZero will implicitly set tabIndex = 0
for these, if the tab index isn't otherwise given.
The :delegate
option indicates that the component wraps some kind of control.
This causes the component's first focusable child to be focused in place of the
component itself. See
delegatesFocus
for details. Warning: if changed in a hot reload, the new value won't apply
to component instances that existing prior to the change.
:inherit-doc-css?
If truthy, SubZero will check the top-level document for stylesheet <link>
elements, and import linked stylesheets into this component. Note that this
wraps the stylesheets with CSSStyleSheet
, which ignores imports.
:form-associated?
If truthy, the component produced will be form associated. Allowing it to report
a current form value, errors, etc via the special :#internals
prop on :root>
(see below).
SubZero uses a markup notation similar to that of
Hiccup. This is the notation that
should be produced by component :view
functions, or passed into the HTML
rendering functions.
In brief, most values are stringified and treated as text. The following are the exceptions.
Vectors represent elements. They should have a keyword (representing the element tag) as the first value. Following that, either a prop map; or a keyword-value sequence or props can be given. Anything that follows makes up the element body.
[:div]
;; -> <div></div>
[:div :id "my-div" :class "foo" "The " [:b "body"]]
;; -> <div id="my-div" class="foo">The <b>body</b></div>
[:div {:id "my-div" :class "foo"} "The " [:b "body"]]
;; -> <div id="my-div" class="foo">The <b>body</b></div>
Sequences are flattened and expanded inline.
[:ul (map (fn [x] [:li x]) ["fee" "fi" "fo" "fum"])]
;; -> <ul><li>fee</li><li>fi</li><li>fo</li><li>fum</li></ul>
This means nil
isn't rendered at all.
Functions are called (passed the prop map) and their returned markup rendered as normal. When combined with tags (see below), this is a powerful tool for optimization.
SubZero recognizes some special keys that can be given in an element's prop map, which have special behavior. The following special props apply to all elements.
:#style
- Sugar for the regular :style
prop. Renders a map of style
properties (e.g {:display :none :color :red}
).:#class
- Sugar for the regular :class
prop. Accepts a string, keyword,
or symbol; or a collection of the same. Flattens, stringifies, and joins the
values together into a class list.:#on
- Registers a map of event listeners (e.g {:click my-click-fn :focus my-focus-fn}
). Multiple listeners for the same event can be specified by
namespacing the keywords (e.g :0/click first-click-fn :1/click second-click-fn
).:#bind
- Creates reactive bindings between regular props (i.e no #
prefix) and IWatchable
things. When the watchable thing updates, SubZero
will update the bound prop in response. If the watchable thing is also
derefable then it'll be deref'd for an initial value. (e.g [:input :#bind {:value !my-atom} :#on {:input #(reset! !my-atom (-> % .-target .-value))}]
).:#key
- Similar to React keys. Creates a consistent mapping between this
vdom node and a particular DOM element instance.:#tag
- Like an
ETag for vdom
nodes. Used to help optimize rendering. If a node's tag is the same across
renders then SubZero won't need reconcile it. This is a powerful tool for
progressive optimizations.:#opaque?
- Indicates that the contents (body) of this node are rendered by
some other means, so SubZero shouldn't touch it. (e.g [:div :innerHTML "<b>foo</b>" :#opaque? true]
).Regular props are rendered by either setting a matching JavaScript property
(if one is found on the prototype of the element being rendered), or as
attributes. SubZero looks for properties that either match the given prop
name exactly, or match the cammelCase'd form of the prop name. So for example
the innerHTML
JavaScript property can be set either as :innerHTML
or
:inner-html
.
:root>
A component :view
function can return a special [:root> ...]
form as its top
level value. This form is similar to element nodes, except its only handles
special props; and these apply to the component instance itself rather than any
child elements.
The :root>
node shares the :#on
, #:style
, :#tag
, and :#opaque?
props
with regular vnodes, with the following caveats:
:#on
- The listeners are applied to the component's ShadowRoot, on which
SubZero dispatches custom lifecycle events: connect
, render
, update
,
disconnect
.:#style
- Applies the given style properties to the component instance as
defaults, which can be overidden externally.Some additional special props can also be set on this node:
:#css
- A string, URL (js/URL
or java.net.URL
), js/CSSStyleSheet
,
or a collection of the same. If the string starts with http
then it's
treated as a URL. The contents are fetched and wrapped in a
js/CSSStyleSheet
. Otherwise it's treated as CSS content and wrapped directly.
After coersion, the stylesheets are adopted by the component's ShadowRoot.
When rendering to HTML as a declarative shadow DOM, produces <script>
elements instead.:#internals
- A map of fields to set on the component's
ElementInternals
. Also supports special sugar keys:
:#states
- Sets ElementInternals#states
from a collection of keywords:#value
- Sets the form value (only for form associated components):#validity
- Should be a map of {:message ? :anchor ? :report? ?}
. Calls
ElementInternals#setValidity
with the given message and anchor. If :report?
is truthy, also calls ElementInternals#reportValidity
.:#on-host
- Like :#on
, but registers the listeners on the element itself,
rather than its ShadowRoot.Use :#tag
in combination with laziness and function substitution to optimize
rendering performance progressively as bottlenecks are found. A node whose tag
is the same across renders has the following performance advantages:
When rendering lists of vnodes with the same tag. If new items can be added to
the list, or exsiting items re-arranged, then make sure to give each item a
unique :#key
.
You can customize how attributes are serialized and parsed (in both HTML and
custom elements) by registering handlers via reg/reg-attribute-writers
and
reg/reg-attribute-readers
respectively. These take keyval seqs, with the key
for each entry being one of: 1) a component name, 2) :default
, 3) a wildcard
pattern like :ns-to-match/*
.
(defn json-reader
[attribute-string attribute-name component-name]
(js/JSON.parse attribute-string))
(defn json-writer
[attribute-value attribute-name component-name]
(js/JSON.stringify attribute-value))
(reg/reg-attribute-readers :my-app/* json-reader :other-component json-reader)
(reg/reg-attribute-writers :my-app/* json-writer :other-component json-writer)
Join the #zero-lib Clojurians channel or open an issue.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close