UNPKG

mreframe

Version:

A reagent/re-frame imitation that uses Mithril instead

180 lines (151 loc) 8.02 kB
{identical, eqShallow, isArray, keys, getIn, merge, assocIn, identity} = require './util' {atom, deref, reset, swap} = require './atom' _mount_ = _redraw_ = _mithril_ = identity _fragment_ = second = (a, b) => b exports._init = (opts) => _mithril_ = opts?.hyperscript || _mithril_ _fragment_ = _mithril_.fragment || second _redraw_ = opts?.redraw || _redraw_ _mount_ = opts?.mount || _mount_ undefined _vnode = null # contains vnode of most recent component _renderCache = new Map ### Reset function components cache. ### exports.resetCache = => _renderCache.clear() _propagate = (vnode, ratom, value) => while vnode vnode.state._subs.set ratom, value vnode = vnode._parent value _eqArgs = (xs, ys) => (not xs and not ys) or (xs?.length is ys?.length and eqShallow(xs._meta, ys._meta) and xs.every (x, i) => eqShallow x, ys[i]) _detectChanges = (vnode) -> not _eqArgs(vnode.attrs.argv, @_argv) or # arguments changed? ((subs = Array.from(@_subs)).some ([ratom, value]) => ratom._deref() isnt value) or # ratoms changed? (subs.forEach(([ratom, value]) => _propagate vnode._parent, ratom, value); no) # no changes, propagating ratoms _rendering = (binding) => (vnode) -> _old = _vnode; _vnode = vnode try @_subs.clear() @_argv = vnode.attrs.argv # last render args binding.call @, vnode finally _vnode = _old _fnElement = (fcomponent) => unless _renderCache.has fcomponent component = oninit: (vnode) -> @_comp = component # self @_subs = new Map # input ratoms (resets before render) @_atom = ratom() # state ratom; ._subs should work for it as well @_view = fcomponent undefined onbeforeupdate: _detectChanges view: _rendering (vnode) -> x = @_view.apply vnode, (args = vnode.attrs.argv[1..]) asElement if typeof x isnt 'function' then x else (@_view = x).apply vnode, args _renderCache.set fcomponent, component _renderCache.get fcomponent _meta = (meta, o) => if typeof o is 'object' and not isArray o then [merge o, meta] else [meta, asElement o] _moveParent = (vnode) => if vnode.attrs vnode._parent = vnode.attrs._parent or null # might be undefined if not called directly from a component delete vnode.attrs._parent vnode ### Converts Hiccup forms into Mithril vnodes ### exports.asElement = asElement = (form) => if isArray form head = form[0] meta = {...(form._meta or {}), _parent: _vnode} if head is '>' then _createElement form[1], (_meta meta, form[2]), form[3..].map asElement else if head is '<>' then _moveParent _fragment_ meta, form[1..].map asElement else if typeof head is 'string' then _createElement head, (_meta meta, form[1]), form[2..].map asElement else if typeof head is 'function' then _createElement (_fnElement head), [{...meta, argv: form}] else _createElement head, [{...meta, argv: form}] else form ### Mounts a Hiccup form to a DOM element ### exports.render = (comp, container) => _mount_ container, view: => asElement comp ### Adds metadata to the Hiccup form of a Reagent component or a fragment ### exports.with = _with = (meta, form) => form = form[..]; form._meta = meta; form ### Creates a class component based on the spec. (It's a valid Mithril component.) Only a subset of the original reagent functons is supported (mostly based on Mithril hooks): constructor, getInitialState, componentDidMount, componentDidUpdate, componentWillUnmount, shouldComponentUpdate, render, reagentRender (use symbols in Wisp). Also, beforeComponentUnmounts was added (see 'onbeforeremove' in Mithril). Instead of 'this', vnode is passed in calls. NOTE: shouldComponentUpdate overrides Reagent changes detection ### exports.createClass = (spec) => bind = (k, method=spec[k]) => method and ((vnode, args) => _vnode = vnode try method.apply vnode, args or [vnode] finally _vnode = null) component = oninit: (vnode) -> @_comp = component @_subs = new Map @_atom = ratom bind('getInitialState')? vnode bind('constructor')? vnode, [vnode, vnode.attrs] undefined oncreate: bind 'componentDidMount' onupdate: bind 'componentDidUpdate' onremove: bind 'componentWillUnmount' onbeforeupdate: bind('shouldComponentUpdate') or _detectChanges onbeforeremove: bind 'beforeComponentUnmounts' view: _rendering(spec.render or do (render = spec.reagentRender) => (vnode) -> asElement render.apply vnode, vnode.attrs.argv[1..]) # component RAtom = (@x) -> @_deref = (=> @x); undefined # ._deref doesn't cause propagation deref.when RAtom, (self) => _propagate _vnode, self, self._deref() reset.when RAtom, (self, value) => if identical value, self.x then value else self.x = value _redraw_() value ### Produces an atom which causes redraws on update ### exports.atom = ratom = (x) => new RAtom x RCursor = (@src, @path) -> @_deref = (=> @src @path); undefined deref.when RCursor, (self) => _propagate _vnode, self, self._deref() reset.when RCursor, (self, value) => if identical value, self._deref() then value else self.src self.path, value _redraw_() value _cursor = (ratom) => (path, value) => # value is optional but undefined would be replaced with fallback value anyway if value is undefined then getIn ratom._deref(), path else swap ratom, assocIn, path, value ### Produces a cursor (sub-state atom) from a path and either a r.atom or a getter/setter function ### exports.cursor = (src, path) => new RCursor (if typeof src is 'function' then src else _cursor src), path ### Converts a Mithril component into a Reagent component ### exports.adaptComponent = (c) => (...args) => _with _vnode?.attrs, ['>', c, ...args] ### Merges provided class definitions into a string (definitions can be strings, lists or dicts) ### exports.classNames = classNames = (...classes) => cls = classes.reduce ((o, x) => x = "#{x}".split ' ' unless typeof x is 'object' merge o, (unless isArray x then x else merge ...x.map (k) => k and [k]: k) ), {} (keys cls).filter((k) => cls[k]).join ' ' _quiet = (handler) => if typeof handler isnt 'function' then handler else (event) -> event.redraw = no; handler.call @, event _quietEvents = (attrs, o = {}) => (o[k] = if k[..1] isnt 'on' then v else _quiet v) for k, v of attrs o prepareAttrs = (tag, props) => if typeof tag isnt 'string' then props else ['class', 'className', 'classList'].reduce ((o, k) => o[k] and o[k] = classNames o[k]; o), _quietEvents props _createElement = (type, first, rest) => # performance optimization _rest = if first[1]?.attrs?.key? then rest else [rest] _moveParent _mithril_ type, (prepareAttrs type, first[0]), first[1], ..._rest ### Invokes Mithril directly to produce a vnode (props are optional if no children are given) ### exports.createElement = (type, props, ...children) => _createElement type, [props or {}], children ### Produces the vnode of current (most recent?) component ### exports.currentComponent = => _vnode ### Returns children of the Mithril vnode ### exports.children = children = (vnode) => vnode.children ### Returns props of the Mithril vnode ### exports.props = props = (vnode) => vnode.attrs ### Produces the Hiccup form of the Reagent component from vnode ### exports.argv = argv = (vnode) => vnode.attrs.argv ### Returns RAtom containing state of a Reagent component (from vnode) ### exports.stateAtom = stateAtom = (vnode) => vnode.state._atom ### Returns state of a Reagent component (from vnode) ### exports.state = (vnode) => deref stateAtom vnode ### Replaces state of a Reagent component (from vnode) ### exports.replaceState = (vnode, newState) => reset (stateAtom vnode), newState ### Partially updates state of a Reagent component (from vnode) ### exports.setState = (vnode, newState) => swap (stateAtom vnode), merge, newState