tape.mvc
An alternative interface to Re-Frame (and Reagent) - via an Integrant system built out of a tape.module system (ported from Duct Framework).
You must be familiar with Reagent, Re-Frame, Integrant, tape.refmap &
tape.module before proceeding.
Your project directory structure should be similar to:
blog
├── deps.edn
├── resources
│ └── blog
│ └── config.edn
└── src
└── blog
├── core.cljs
└── app
├── layouts
│ └── app.cljs
├── users
│ └── ...
└── posts
├── controller.cljs
├── model.cljs
└── view.cljsUnder blog.app you should have a package for each piece of your UI (users,
posts). Each of these will have a model, view and controller namespace (not
necessarily all 3).
The model is a normal namespace, and is likely to be missing if you have little to no business logic for that UI piece.
The controller namespaces contain functions that can be registered in
Re-Frame. Each fn that is to be registered is annotated in metadata with
^{::mvc/reg <kind>} where <kind> is one of:
-
::mvc/event-fxevent handler with co/effect i/o -
::mvc/event-dbevent handler with app-db i/o -
::mvc/subsubscription -
::mvc/sub-rawraw subscription -
::mvc/fxeffect handler -
::mvc/cofxcoeffect handler
To provide interceptors to event handlers, or signals to subscriptions, use the following metadata:
-
::mvc/interceptorsvector of symbols that evaluate to interceptors in the current namespace -
::mvc/signalsvector of symbols that evaluate to signals in the current namespace
Metadata at the namespace level is added to the functions that are registered, and can be used to:
-
add interceptors to all event handlers in a namespace,
-
or a signal to all subscriptions in the namespace.
Function metadata directives override namespace metadata ones.
(ns blog.app.greet.controller
(:require [tape.mvc :as mvc :include-macros true]))
(defn hello
{::mvc/reg ::mvc/event-db}
[_db [_ev-id _params]]
{::say "Hello Tape MVC!"})
(defn say
{::mvc/reg ::mvc/sub}
[db _query]
(::say db))
(mvc/defm ::module)There is a (c/defm ::module) call at the end that inspects the current
namespace and defines a tape.module that will have the functions be registered
in Re-Frame. It is equivalent to:
(derive ::hello ::mvc/event-db)
(derive ::say ::mvc/sub)
(defmethod ig/init-key ::module [_ _]
(fn [config]
(module/merge-configs config {::hello hello, ::say say})))Derived keys are collected by tape.refmap and registered in Re-Frame.
The view namespace contains Reagent functions. Some can be registered in the views map if they are annotated with:
-
^{::mvc/reg ::mvc/view}if the function is to be registered in the views registry map
The key in the map will be based off the controller namespace, as to match an
event (see naming conventions below) that can select a view; example:
{::greet.c/hello greet.v/hello}.
(ns blog.app.greet.view
(:require [re-frame.core :as rf]
[tape.mvc :as mvc :include-macros true]
[blog.app.greet.controller :as greet.c]))
(defn hello
{::mvc/reg ::mvc/view}
[]
(let [say @(rf/subscribe [::greet.c/say])]
[:p say]))
(mvc/defm ::module) ;; blog.app.greet.controller must existThere is a (mvc/defm ::module) call at the end that inspects the current
namespace and defines a tape.module that will have the functions be registered
in the views registry map. It is equivalent to:
(derive ::hello ::mvc/view)
(defmethod ig/init-key ::module [_ _]
(fn [config]
(module/merge-configs
config
{::hello ^{::mvc/controller-ns-str "blog.app.greet.controller"} hello})))Derived keys are collected by tape.refmap and registered in the view registry.
(mvc/defm ::module) macros can also be called with a map argument that’s merged in
the module configuration output. This allows plain integrant components to be
defined. It’s more verbose than the plain function approach, but we can inject
dependencies from the system map via ig/ref & such.
(...)
(defn hello
{::mvc/reg ::mvc/event-db}
[_db [_ev-id _params]]
{::say "Hello Tape MVC!"})
(defmethod ig/init-key ::say [_ some-db]
(fn [_ _] (ratom/reaction (::something @some-db))))
(derive ::say ::mvc/sub-raw)
(mvc/defm ::module {::say (ig/ref ::some-ns/some-db)})To allow IDE navigation, we have two macros that proxy to Re-Frame:
(mvc/dispatch [posts.c/index])
;; => (rf/dispatch [::posts.c/index])
(mvc/subscribe [posts.c/posts])
;; => (rf/subscribe [::posts.c/posts])In their use, the macros accept events with a symbol form (that can be navigated via IDE), but once compiled, they are in the standard Re-Frame API with no performance penalty. Added value: the handler existence is checked at compile time, and typos are avoided.
In resources/blog/config.edn declare your modules that will
tape.module/fold-modules into your Integrant system’s config-map:
{:tape.profile/base {}
:tape.mvc/module nil
:blog.app.greet.controller/module nil
:blog.app.greet.view/module nil}In src/blog/core.cljs you tape.module/read-config and
tape.module/exec-config your Integrant system:
(ns tape.blog
(:require
[goog.dom]
[reagent.core :as r]
[re-frame.core :as rf]
[tape.module :as module :include-macros true]
[tape.mvc :as mvc]
[tape.tools.current.controller :as current.c]
[blog.app.greet.controller :as greet.c]
[blog.app.greet.view :as greet.v]))
(module/load-hierarchy)
(def config (module/read-config "tape/blog/config.edn"))
(defonce system nil)
(defn app [] [:div @(rf/subscribe [::current.c/view-fn])])
(defn -main []
(set! system (-> config module/prep-config ig/init))
(rf/dispatch-sync [::greet.c/hello])
(r/render-component [app] (goog.dom/getElement "app")))Note naming convention on requires:
-
controllers:
[blog.app.posts.controller :as posts.c] -
views:
[blog.app.posts.view :as posts.v]
Note the exclusive use of namespaced keywords and the naming conventions:
-
(if
tape.routeris used) the route named::posts.c/indexdispatches -
the event with the id
::posts.c/indexwhich -
is handled by the
posts.c/indexhandler -
and (if
tape.currentis used) renders theposts.v/indexview (if it exists, and the view was not already set from the handler) -
by setting in app-db
{::current.c/view ::posts.c/index}(automatically via the view interceptor, or manually in the event handler) -
which results in the subscription
(rf/subscribe [::current.c/view-fn]) -
to yield the
posts.v/indexview fn