Skip to content

Latest commit

 

History

History
489 lines (398 loc) · 13.7 KB

File metadata and controls

489 lines (398 loc) · 13.7 KB

Tutorial

This tutorial is meant to make you familiar with Tape Framework. It’s not a comprehensive reference.

Prerequisites

Before learning how to use Tape Framework you need to be familiar with: Reagent, Re-Frame, Integrant, Integrant REPL, clj-new, Figwheel, Reitit.

Generate an app

We generate an app by means of tape.clj-template; we assume clj-new is installed and configured.

Until the first release you will have to first clone each subproject:

shell
mkdir tape
cd tape
git clone git@github.com:tape-framework/versions.git
git clone git@github.com:tape-framework/refmap.git
git clone git@github.com:tape-framework/module.git
git clone git@github.com:tape-framework/mvc.git
git clone git@github.com:tape-framework/tools.git
git clone git@github.com:tape-framework/toasts.git
git clone git@github.com:tape-framework/dev.git
git clone git@github.com:tape-framework/clj-template.git

Now generate an app:

shell
CLJ_CONFIG=./versions/ clj -A:versions \
  -Sdeps '{:deps {tape/clj-template {:local/root "./clj-template"}}}' \
  -X:new clj-new/create :template tape :name myname/myapp

This will generate the following files tree:

files
myapp
├── deps.edn
├── dev
│   └── src
│       └── cljs
│       │   └── user.cljs
│       └── myname
│           └── myapp
│               └── dev.cljs
├── resources
│   └── public
│   │   └── index.html
│   │   └── favicon.ico
│   │   └── css
│   │       └── style.css
│   └── myname
│       └── myapp
│           └── config.edn
└── src
    └── myname
        └── myapp
            ├── core.cljs
            └── app
                ├── layouts
                │   └── app.cljs
                └── hello
                    ├── controller.cljs
                    └── view.cljs

Open the REPL

First, start a Clojure REPL:

shell
cd myapp
CLJ_CONFIG=../versions/ clj -A:versions:test # dev tools are in the test alias

Then, start a ClojureScript REPL via Figwheel:

repl
(build/fig) ;; starts Figwheel and opens a browser at http://localhost:9500/

Development tooling based on Integrant REPL is in myname.myapp.dev namespace:

repl
(in-ns 'myname.myapp.dev) ;; (repl/halt), (go), (repl/reset) etc.

Note we assume the following namespaces below:

(:require
 [reagent.core :as r]
 [re-frame.core :as rf]
 [tape.mvc :as mvc :include-macros true]
 [tape.router :as router]
 [tape.tools.current.controller :as current.c]
 [tape.toasts.controller :as toasts.c]
 [tape.toasts.view :as toasts.v]
 [myname.myapp.app.hello.controller :as hello.c]
 [myname.myapp.app.hello.view :as hello.v])

Layouts and current view

The generated app has in app/layouts/app.cljs a reagent function called app. This is mounted in the DOM and is the entrypoint to our app. In it, we define an HTML page layout and render the "current view".

app/layouts/app.cljs
(defn app []
  (let [current-view-fn @(rf/subscribe [::current.c/view-fn])]
    [:<>
     [:section.section
      [:div.container
       (when (some? current-view-fn)
         [current-view-fn])]]
     [toasts.v/index]]))

The "current page" that changes alongside the URL (whether via hash-change or History API) is so established on the web that we decided to make it available by default in Tape. You don’t have to necessarily use it, but if you want to, it’s available.

The "current view" is a reagent view function that can be changed by manipulating ::current.c/view entry in app-db. It’s value must be a namespaced keyword mapping to a controller handler (more on that later):

app-db
{::current.c/view ::hello.c/index} ;; points to hello.v/index

Note that we use the controller in the value. There are two existing subscriptions:

(rf/subscribe [::current.c/view]) ;; yields the keyword ::hello.c/index
(rf/subscribe [::current.c/view-fn]) ;; yields the function hello.v/index

Controllers

Controllers are namespaces with Re-Frame handlers, but these are not registered directly to Re-Frame. We use a special DSL. The generated app has a sample controller in myname/myapp/app/hello/controller.cljs. In it we have an event handler:

app/hello/controller.cljs
(defn index
  {::mvc/reg ::mvc/event-db}
  [_db [_ev-id _params]]
  {::say "Hello Tape Framework!"})

At startup, we dispatch the [::home.c/index] event that’s handled by this handler (events are always namespaced and there is a correspondence between event names and handler names). Let’s add another one below it that changes the greeting:

app/hello/controller.cljs
(defn change
  {::mvc/reg ::mvc/event-db}
  [db [_ev-id _params]]
  (assoc db ::say "Hello World!"))

We also have a subscription in our hello controller for our greeting:

app/hello/controller.cljs
(defn say
  {::mvc/reg ::mvc/sub}
  [db _query] (::say db))

At the end we call the (mvc/defm ::module) macro that inspects the namespace and defines a tape.module. This is added in the modules config map by "modules discovery".

When a controller namespace is required in another namespace, the naming convention is as follows:

(:require [myname.myapp.app.hello.controller :as hello.c])

Views

Views are namespaces with Reagent functions. Functions that can become the "current view" must be annotated with ^::mvc/view and we call them view functions.Reagent functions that are not annotated we call partials. The generated app has a sample view in myname/myapp/app/hello/view.cljs. In it, we have Reagent function:

app/hello/view.cljs
(defn index
  {::mvc/reg ::mvc/view}
  []
  (let [say @(rf/subscribe [::hello.c/say])]
    [:p say]))

There can be a name correspondence between a controller event handler and a view function, in our case here: hello.c/indexhello.v/index. If there is such a correspondence, after the handler runs, if there is no ::current.c/view in set app-db, our view function is automatically set as current (by an interceptor).

At the end we call the (mvc/defm ::module) macro that inspects the namespace and defines a tape.module. This is added in the modules config map by "modules discovery".

Let’s add a button that calls our hello.c/change handler:

app/hello/view.cljs
(defn index
  {::mvc/reg ::mvc/view}
  []
  (let [say @(rf/subscribe [::hello.c/say])]
    [:p say]
    [:button.button {:on-click #(rf/dispatch [::hello.c/change])}]))

When a view namespace is required in another namespace, the naming convention is as follows:

(:require [myname.myapp.app.hello.view :as hello.v])

Routing

The tape.router module adds routing capabilities based on [Reitit](https://github.com/metosin/reitit). Routes are defined in controllers at the beginning in a var named routes. Our sample hello controller has the following routes:

app/hello/controller.cljs
(def ^{::mvc/reg ::mvc/routes} routes
  ["" {:coercion rcs/coercion}
   ["/say" ::index]])

When the URL of the page changes (via hash-change or History API - depending on how the router is configured in the config map - see myname/myapp/core.cljs) and event corresponding to the matching route is dispatched. In our case above when the page navigates to http://localhost:9500/#/say the event [::hello.c/index params] is dispatched. The params contains path and query params matched by the route. Let’s add a new route to our change handler:

app/hello/controller.cljs
(def ^{::mvc/reg ::mvc/routes} routes
  ["" {:coercion rcs/coercion}
   ["/say" ::index]
   ["/change/:to" ::change]])

And let’s make our change handler aware of the :to param:

app/hello/controller.cljs
(defn change
  {::mvc/reg ::mvc/event-db}
  [db [_ev-id params]]
  (assoc db ::say (-> params :path :to)))

We can now change our button in the view to a link:

app/hello/view.cljs
(defn index
  {::mvc/reg ::mvc/view}
  []
  (let [say @(rf/subscribe [::hello.c/say])]
    [:p.hello-tape say]
    [:a.button {:href (router/href* [::hello.c/change {:to "Wazaaa"}])}]))

We used the name of the route and params, and Reitit assembled the route path for us (a sort of reverse routing). When we click the link the browse address changes to "localhost:9500/#/change/Wazaaa". This is matched by the router dispatching [::hello.c/change {:path {:to "Wazaaa"}}]. This is handled by hello.c/change and the greeting is changed.

Timeouts and Intervals

To be idiomatic when setting timeouts and intervals tape.tools offers the following modules for your config.edn:

resources/myname/myapp/config.edn
{:tape.tools.timeouts.controller/module nil
 :tape.tools.intervals.controller/module nil}

To set a timeout dispatch the following event:

(rf/dispatch [::timeouts.c/set
              {:ms 3000 ; miliseconds
               :set [::was-set] ; optional, dispatched as [::was-set timeout-id]
               :timeout [::timed-out]}]) ; dispatched on timeout

To clear a timeout: (rf/dispatch [::timeouts.c/clear timeout-id]). Similar for intervals.

Working with forms

A form with a number of fields is mapped to a hash-map in app-db. Let’s say we have a login form with an email and password.

Form controller

We start by creating a controller with:

  1. An event handler that sets a pair in the map.

  2. A subscription that reads the map.

app/login/controller.cljs
(ns myname.myapp.app.login.controller
  (:require [tape.mvc :as mvc :include-macros true]))

(defn field
  "Assoc in app-db login map value v at key k."
  {::mvc/reg ::mvc/event-db}
  [db [_ k v]] (assoc-in db [::login k] v))

(defn login
  "Subscription to the login map"
  {::mvc/reg ::mvc/sub}
  [db _] (::login db))

(mvc/defm ::module)

Form view

In our corresponding view, we make a form-fields partial that will render the form fields. Using tape.tools lens we make a function that "gets" by reading the subscription and "sets" by dispatching an event. Using tape.tools’s `form/field we make inputs that control our hash-map via the above lens function. Finally, we attempt login by dispatching an event if the HTML5 Validation API doesn’t complain, via form/when-valid.

app/login/view.cljs
(ns myname.myapp.app.login.view
  (:require [re-frame.core :as rf]
            [tape.mvc :as mvc :include-macros true]
            [tape.tools :as tools]
            [tape.tools.ui.form :as form :include-macros true]
            [myname.myapp.app.guis.login.controller :as login.c]))

(defn- form-fields []
  (let [lens (mvc/lens* ::login.c/login  ;; the subscription
                        ::crud.c/field)] ;; the handler
    [:<>
     [:div.field
      [:label.label "Email"]
      [:div.control
       [form/field {:type :text, :class "input", :source lens,
                    :field :email, :required true}]]]
     [:div.field
      [:label.label "Password"]
      [:div.control
       [form/field {:type :password, :class "input", :source lens,
                    :field :password, :required true}]]]]))

(defn new
  {::mvc/reg ::mvc/view}
  []
  [:form
   [:h2 "Login"]
   [form-fields]
   [:div.field
    [:div.control
     [:button.button.is-primary
      {:on-click (form/when-valid #(rf/dispatch [::login.c/create]))}
      "Log in"]]]])

(mvc/defm ::module) ;; myname.myapp.app.guis.login.controller must exist

Toasts notifications

Generally you want to notify the user of success/failure of various actions. The tape.toasts module allows you to flash such messages. If you used the Tape Framework app generator the module is already configured. To show a message dispatch (rf/dispatch [::toasts.c/create :info "Some message"]). Toast kind can be one of: :success, :danger,:warning, :info. Example in controller handler:

app/some/controller.cljs
(defn create
  {::mvc/reg ::mvc/event-fx}
  [{:keys [db]} _]
  {:db         (update db ::people conj (::person db))
   :dispatch-n [[::router/navigate [::index]]
                [::toasts.c/create :success "Person added!"]]})

Ergonomic API

When dispatching and subscribing, we have a number of macros called the "Ergonomic API". These are equivalent to the ones in Re-Frame (or Tape - ending in *), except they take a handler’s symbol instead of a keyword in the first position of the vector. This symbol is IDE navigable ("jump to definition"). The macros macroexpand to the standard API, thus have no runtime cost. Also added value: the handler existence is checked at compile time, and typos are avoided.

(mvc/dispatch [posts.c/index])
;; => (rf/dispatch [::posts.c/index])

(mvc/subscribe [posts.c/posts])
;; => (rf/subscribe [::posts.c/posts])

(router/href [greet.c/hi {:say "Hi!"}])
;; => (router/href* [::greet.c/hi {:say "Hi!"}])

(router/navigate [greet.c/hi {:say "Hi!"}])
;; => (router/navigate* [::greet.c/hi {:say "Hi!"}])

(mvc/lens posts.c/post posts.c/field)
;; => (mvc/lens* ::posts.c/post ::posts.c/field)

License

Copyright © 2020 clyfe

Distributed under the MIT license.