##Sketch for Enliven Component Model
This design page is a sketch of how I see enliven being used for dynamic templates in the browser.
- All state is in an application state object.
- State is scoped through lenses.
- Users should not have to manage lenses manually.
- There is no tree walking; paths/lenses, for dom manipulation, are determined at compile time.
- Dom nodes are pre-rendered where possible and only areas where change occurs get re-rendered.
- App state is changed through events.
dynamic-templates take html from either a resource or string and a set of transforms.
Transforms are similar to enlive with selector+transform pairs.
Note: we may need some way to pass in a set of lenses/paths that point to the relevant application scope and relevant transient scope.
;; html - "<h1>test</h1>" or Resource
;; transformations pairs
(dynamic-template [html lens-paths & transforms] ...)
return value
;;the base app-data needs to be an atom if not accessed trough a lens
;;might need to pass in a set of optional lenses here
(fn [root data] ...)
I see templates/components as a series of nested structures that update the application state by propagating changes through a set of nested lenses. We need to provide helper functions that make it easy for them to create and nest the lenses. In this gist you use scope as a way of specifying the lens for an event. We might need to use something similar for sub-templates/components.
(def my-button
(dynamic-template "<div><button id="my-btn"></button><div>"
:button (scope :done
(on-click [e done] (not done)))))
(def parent-comp
(dynamic-template "<div><h1>button below</h1><span id="tmp></span></div>"
:#tmp (scope :button-data
(attach-template my-button)))))
I would suggest we use the same model as om when it comes to rendering. Rendering should be async and only done at requestAnimationFrame. With the scope functionality and a list of paths changed at each iteration, we can ask the components to update. Each component/template will contain logic that checks paths used and updated only those transforms affected.
###Anatomy of a Component
The design of the component is meant to allow eliven to manage the scope/lenses into the application state and abstract away the handling of internal component state.
(defprotocol Component
(update [this dirty-path-set app-lens component-lens] )
(initialize [this app-lens component-lens])
(register-child [cstate scope child]))
;;this is returned by dynamic-template
(fn [root app-state]
(reify
Component
(update [this dirty-path-set app-lens component-lens]
(let [cstate (fetch component-lens)
astate (fetch app-lens)
(do-seq [path (keys (:transforms (component-lens app-state)))]
(when (contains? dirty-path-set path)
(modify-ref this cstate astate app-state path)))
(update-children dirty-path cstate astate)))
(initialize [this app-lens component-lens]
;this is generated code with an accumulator for transforms
(let [transforms (render-intial-dom ...)]
(->> transforms
(push component-lens :transforms)
(update this transforms app-lens))))
(register-child [cstate scope child]
(let [child-id (gen-id)]
;;register child creates the comp lens, app lens
;;and calls initialize on the child comp
(register-child child-id cstate scope child)))))
;; bind-root is where it all begins in well formed app this would be at the body
;;this adds the watcher on the app-state atom
;;this also initialized the first component and app lenses
(bind-root state component root)
;;helper functions
(modify-ref [comp {:keys [node rules]} app-sate]
(execute-plan rules node (find-in-path data path)))
The general idea is correct. However I think components will be transforms. No need to
attach-template
them.See https://gist.github.com/cgrand/8553906