reagent
module
Based on ClojureScript reagent library; used for writing a simplified interface
to Mithril components (mostly for defining components as functions returning view
in Hiccup format, as well as tying redraws to state changes).
Short overview of Hiccup format (in mreframe/Mithril context):
['tag#id.class1.class2', props, ...children]
is eqivalent tom('tag#id.class1.class2', props, ...children)
(nesting notation of'tag>child1>child2'
is currently not supported)['>', Component, props, ...children]
is equivalent tom(Component, props, ...children)
['<>', ...items]
is equivalent to[...items]
r.with(meta, ['<>', ...items])
is equivalent tom.fragment(meta, [...items])
(r.with({key: id}, ['<>', foo, bar])
is equivalent to^{:key id} [:<> foo bar baz]
in Clojure)[component, ...args]
is equivalent tom(component, {}, ...args)
(withcomponent(...args)
called internally)r.with(meta, [component, ...args])
is similarly equivalent tom(component, meta, ...args)
Suggested imported module name is r
.
Compared to regular Mithril components, each of Reagent components will have a RAtom (r.atom
) defined in state
of its vnode; additionally, they can be defined as functions returning Hiccup (or functions returning such functions).
The vnode of current component can be accessed at runtime by calling r.currentComponent()
.
Since JS doesn’t support adding metadata easily, you can use r.with
to supply props
(particularly key
) to Reagent components or to
fragments. For tags and adapted components, use the first argument (like in Mithril).
_init (opts)
Is a setup function (only necessary if you’re using nodeps
bundle); opts
may include:
redraw
: redraw hook function (defaults tom.redraw
);mount
: vnode mount function (defaults tom.mount
);hyperscript
: vnode generation function (defaults tom
;.fragment
is used for rendering fragments if present);
Only included options are updated, so if you need to disable redraw for some reason you can simply call r._init({redraw: identity})
.
resetCache ()
Clears function-components cache (shouldn’t really be necessary).
atom (x)
Creates an atom; it calls the redraw
hook on every successful data update
(but doesn’t update when setting the same value).
var x = r.atom(42) // ⇒ RAtom(42)
reset(x, {answer: 42}) // ⇒ {answer: 42} /* m.redraw() is called on update */
reset(x, {answer: 42}) // ⇒ {answer: 42} /* m.redraw() is not called as the value has not changed */
cursor (src, path)
creates a derived atom; it also skips updates when new value is the same as the old one
- if
src
is a function,deref(rcursor)
returnssrc(path)
, andreset(rcursor, value)
callssrc(path, value)
; - otherwise,
deref()
returnsgetIn(deref(src), path)
, andreset()
callsswap(src, assocIn, path, value)
.var x = r.atom({foo: {bar: 42, baz: 5}}), y = r.cursor(x, ['foo', 'bar']) deref(y) // ⇒ 42 reset(y, 12) // ⇒ 12 /* x was updated, so m.redraw() is called */ deref(x) // ⇒ {foo: {bar: 12, baz: 5}} reset(y, 12) // ⇒ 12 /* x wasn't updated, so m.redraw() isn't called */ var inBounds = (([l, r], v) => (v ? reset(y, v) : Math.max(l, Math.min(r, deref(y))))), z = r.cursor(f, [1, 10]) deref(z) // ⇒ 10 reset(z, 4) // ⇒ 4 deref(x) // ⇒ {foo: {bar: 4, baz: 5}}
classNames (...classes)
Combines multiple CSS class definitions into a class
attribute value (a definition can be a string, a list, or a dictionary).
r.classNames("foo bar", [1 && 'x', 0 && 'y', 'z'],
{answer: 42, foo: null, error: false}) // ⇒ "bar x z answer"
createElement (type, props?, ...children)
Invokes Mithril directly to produce a vnode (props
are optional when there’s no children); when props
are provided,
each of Mithril CSS class (class
, className
& classList
) attributes is replaced using classNames()
(unless it’s nil).
Note: for performance, children are passed to Mithril as an array (fragment with no metadata).
r.createElement('div', {class: ['foo', x && 'bar', 'baz']}, "Hello World")
// ~ m('div', {class: r.classNames(['foo', x && 'bar', 'baz'])}, "Hello World")
Note regarding classList
: in Mithril, it works same way as class
or className
(accepts a string and not a list),
except that including classes in tag selector (e.g. div.foo
) overrides it instead of it being appended to generated CSS classes.
adaptComponent (c)
Converts a Mithril component into a Reagent component.
Its arguments are passed to the vnode as children
, and metadata is passed as attrs
.
var x = r.adaptComponent({view: ({attrs, children}) => m('div', attrs, "Hello, World", children)}),
y = () => r.with(meta, [x, 1, 2, 3]) // ~ ['>', x, meta, 1, 2, 3]
asElement (form)
Converts Hiccup form(s) into Mithril vnodes.
r.asElement(['>', mComponent, 1, 2, 3]) // ⇒ m(mComponent, 1, 2, 3)
r.asElement([rComponent, 1, 2, 3]) // ⇒ m(/*rComponent*/, {}, 1, 2, 3)
r.asElement(['<>', ['div', {class: 'foo'}, 42]]) // ⇒ m.fragment({}, [m('div', {class: 'foo'}, 42)])
with (meta, form)
Adds metadata (meta
) to the form
of a fragment or a Reagent component in place of props;
can be used to supply a key
.
r.asElement( r.with({key: 42}, [rComponent, 1, 2, 3]) ) // ⇒ m(/*rComponent*/, {key: 42}, 1, 2, 3)
r.asElement( r.with({key: 42}, ['<>', x && ['div', {id: x}]]) )
// ⇒ m.fragment({key: 42}, [x && ['div', {id: x}]])
createClass (spec)
Creates a Reagent component based on provided hook methods (mostly based on Clojure equivalents; in Wisp, use symbols as keys):
getInitialState
produces initial state of the state atom (runs at the start)constructor
initialises the component (currently runs aftergetInitialState
)componentDidMount
runs after first render to DOMcomponentDidUpdate
runs after a DOM updatecomponentWillUnmount
runs before removal from DOMshouldComponentUpdate
checks if there’s a need for updatebeforeComponentUnmounts
runs pre-removal code/promise for the root of a subtree that is being removedrender
renders a Mithril component (seeview
method)reagentRender
is the same asrender
but renders Hiccup instead (same as function components)
Note: shouldComponentUpdate
overrites Reagent changes detection
For all of these, vnode is bound to this
as well as passed as the first argument (like in Mithril),
and constructor
additionally accepts props as the 2nd argument. The only exception is renderReagent
which only expects function arguments (same as function components).
var canvas = r.createClass({
componentDidMount: ({dom, state}) => {state.gl = dom.getContext('webgl')},
componentWillUnmount: ({state}) => state.gl.getExtension('WEBGL_lose_context').loseContext(),
reagentRender: () => ['canvas'],
})
// ~ {
// oncreate ({dom}) {this.gl = dom.getContext('2d')},
// onremove () {this.gl.getExtension('WEBGL_lose_context').loseContext()},
// view: () => m('canvas'),
// }
render (form, container)
Mounts a Hiccup form as a component onto a DOM element.
r.render([App], document.body)
// ~ m.mount(document.body, {view: () => r.asElement([App])})
currentComponent ()
Returns the vnode of current component (in a function component or a method of a Reagent component).
Note that if you use an unbound function
in either case, the vnode will be accessible as this
.
var rComponent = () => [...r.currentComponent().children] // ~ {view: ({children}) => [...children]}
children (vnode)
Returns children of the Mithril vnode.
var rComponent = () => [...r.children( r.currentComponent() )]
// ~ {view: ({children}) => [...children]}
props (vnode)
Returns props (attrs
) of the Mithril vnode.
var rComponent = () => ['div', r.attrs( r.currentComponent() )]
// ~ {view: ({attrs}) => m('div', attrs)}
argv (vnode)
Returns the Hiccup form passed to the Reagent component.
var rComponent = () => {console.log(r.argv( r.currentComponent() )); return ['div']}
// prints [rComponent, 1, 2, 3] if used as such
stateAtom (vnode)
Returns RAtom containing state of a Reagent component.
var rComponent = r.createClass({
getInitialState: () => ({answer: 42}),
reagentRender: () => ['div', "Answer: ", deref(r.stateAtom( r.currentComponent() )).answer],
})
state (vnode)
Returns state of a Reagent component (same as deref( r.stateAtom(vnode) )
).
var rComponent = r.createClass({
getInitialState: () => ({answer: 42}),
reagentRender: () => ['div', "Answer: ", r.state( r.currentComponent() ).answer],
})
replaceState (vnode, newState)
Replaces state of a Reagent component (same as reset(r.stateAtom(vnode), newState)
).
var rComponent = r.createClass({
getInitialState: () => ({answer: 42}),
reagentRender () {
let x = r.state(this).answer;
return ['div',
"Answer: ", x, " ",
['button', {onclick: () => r.replaceState(this, {answer: x+1})}, "+1"]],
},
})
setState (vnode, newState)
Partially updates state of a Reagent component (same as swap(r.stateAtom(vnode), merge, newState)
).
var rComponent = r.createClass({
getInitialState: () => ({answer: 42, foo: 1}),
reagentRender () {
let x = r.state(this);
return ['div',
"Answer: ", x.answer, " (", x.foo, ") ",
['button', {onclick: () => r.setState(this, {answer: x+1})}, "+1"]],
},
})