Open metasoarous opened 8 years ago
What about having some controller-state
(maybe as a datomic entity) and a representation-middleware-fn
. Each controller can be represented as a set of buttons to display. Then you can mess with the context. So you could change the pull query in the context to only pull certain summary fields with a collapse-controller
. A tab-controller
could have a set of tabs which are stored posh queries which will change which posh query gets called. The names of the tabs will be displayed as buttons. Then you have a container-controller
which puts all the views of the different controllers into a button bar and and represents the main-query run through all the controller-middleware. Also things like an add-controller
which holds the local state for temporary entity before moving it into the normal data flow.
This all sounds great! Some high-level comments/questions:
add-controller
please? I get what you want that to do, but how does it hook into things?Cheers!
I think a control
is just a representation
of a user controlled instrument
. There are two main classes of control
. Those that adjust the data
and those that adjust the layout
. If you haven't seen it already http://nickrossiter.org.uk/process/VisualizationFoundationsIEEE.pdf Fig 14 on p 13 has the exact definitions I'm using here. Basically schema
is to data
as layout
is to representation
. With that in mind we should have a routing function that maps schema
to layout
which will serve as our rules
transformation. A very simple example (using only schema type) might look like:
(def simple-layout {:e.type/todo (fn [app schema data]
[represent :db.type/bool data]
[represent :db.type/string data]
[represent :db.type/string data])})
(def edit-layout {:e.type/todo (fn [app schema data]
[represent :control [instrument app :edit :db.type/boolean data :todo/complete?)]
[represent :control [instrument app :edit :db.type/string data :e/name)])})
(def modal-layout {:e.type/todo (fn [app schema data]
;; ???: could be implemented as layout middleware
(if @modal-state
[layout simple-layout [app schema data]]
[layout edit-layout [app schema data]])
[represent :control [instrument modal-state :toggle])})
(def pull-layout
{:default (fn [app schema data]
[represent (:type schema) data])
:db.type/ref (fn [app schema data]
(for [attr (:attrs schema)]
[layout pull-layout [app (get schema attr) (get data attr)]]))})
(def pull-form-layout
{:default (fn [app schema data]
(for [attr (:attrs schema)]
[represent :control [instrument app :edit (get-type schema attr) (get data attr) attr]]))})
So one thing to think about with all this is that the semantics of how we scope layouts should be quite flexible. Not just by :default
, :db.type/ref
, or :e.type/Todo
etc as keys; It's too easy to want to specify some really specific combination of criteria for applying a layout here. But actually, this has a lot to do with what I've been thinking about how context should work as well. I'm coming to the conclusion that we should be able to use entities with scoping attributes to model the context data we want to apply in one situation versus another. Of course, this leaves the issue of precedence, which is what's been bugging me. A lot of the solutions feel hacky. But I think it's the way to go for a number of reasons. Perhaps we should pick up that line of thought in another issue, and then revisit here.
I totally agree on the flexibility. Anything that is in the schema necessarily needs to be available for layout decisions. My plan was to just route it (using cond as opposed to a map). I'm not entirely grokking the scoping attributes concept. So opening a ticket is probably the right move.
@metasoarous Okay so this code actually runs. Let's talk about what should be different to make this more correct/orthogonal/simple.
(rep/register-representation
::arrow-button
(fn [app _ [state handler]]
[re-com/button
:label ">"
:on-click handler]))
(defn layout-controls [representation-fn]
(fn [app [id {:keys [controls] :as context}] data]
[representation-fn
app
[id
(deep-merge
{:layout {:controls
(for [[k {:keys [represent state handler] :as control}] controls]
^{:key k}
[rep/represent app [represent] [state handler]])}}
context)]
data]))
(defn collapser [representation-fn]
(fn [app [id {:as context
{:keys [pull-summary pull-expression] :as subscriptions} :subscriptions}] data]
(let [collapsed? (r/atom false)
collapse-handler (fn [] (log/info "clicked") (swap! collapsed? not))]
(fn [app [id context] data]
(let [collapsed? @collapsed?
context (deep-merge
{:controls
{::collapser
{:represent ::arrow-button
:state collapsed?
:handler collapse-handler}}}
context
{:subscriptions {:pull-expression (if collapsed? pull-summary pull-expression)}})]
;; (log/info "context:" context)
[representation-fn app [id context] data])))))
(defn t-box [& {{:keys [a b] :as children} :children
:keys [adjustable?]
:or {:adjustable? false}}]
(let [a [re-com/h-box :children a]]
(if adjustable?
[re-com/v-split :panel-1 a :panel-2 b]
[re-com/v-box :children children])))
(defn represent-children-with-controls [layout]
(layout assoc :children [(:controls layout) (:representation layout)]))
(defn rebox-children [layout]
(let [[child child2] (:children layout)]
(layout assoc
:child child
:panel-1 child
;; FIXME: second can be out of bounds
:panel-2 child2)))
(defn box-layout [representation-fn boxer]
(fn [app [id context] data]
[representation-fn app
[id (deep-merge {:layout
{:boxer boxer}}
context)]
data]))
(defn box-layouter [representation-fn]
(fn [app [id {:as context
{:as layout
:keys [boxer controls]} :layout}] data]
[boxer :children [controls [representation-fn app [id context] data]]]))
(defn pull-subscription [representation-fn]
(fn [app [id context] data]
[representation-fn app [id (deep-merge {:subscriptions {:pull-data ((:pull-expression (:subscriptions context)) @data)}}
context)] data]))
(defn subscriber [representation-fn]
(fn [app [id context] layout]
[representation-fn app [id context] (:subscriptions context)]))
(rep/register-representation
::simple
[collapser pull-subscription subscriber layout-controls #(box-layout % t-box) box-layouter]
(fn [app [_ context] {:keys [pull-data]}]
[re-com/label :label (pr-str pull-data)]))
(defn hello [app]
(let [hello-db (r/atom {:hello-all "Hello, World!" :hello-some "Hello."})]
(fn [app]
[rep/represent app [::simple {:subscriptions {:pull-expression :hello-all
:pull-summary :hello-some}}]
hello-db])))
This is pretty open ended... The bottom line though is that you shouldn't have to override a representation to specify which controls something needs. Representations have a lot of gunk in them (passing down context etc), and folks shouldn't have to wade through that just to pick some controls. Maybe controls should be separately registered? What should the shape be? This would let us just specify the namespaced keywords for which controls we wanted, and separately specify how we group them together.