Merge pull request #188 from metosin/frontend-docs-2

Replace :params with :identity/:parameters
This commit is contained in:
Juho Teperi 2019-02-08 10:56:14 +02:00 committed by GitHub
commit 3a8f0cf52d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 166 additions and 67 deletions

View file

@ -1,3 +1,12 @@
## Unreleased
* Frontend controllers:
* Controller `:params` function has been deprecated
* Controller `:identity` function works the same as `:params`
* New `:parameters` option can be used to declare which parameters
controller is interested in, as data, which should cover most
use cases: `{:start start-fn, :parameters {:path [:foo-id]}}`
## 0.2.13 (2019-01-26) ## 0.2.13 (2019-01-26)
* Don't throw `StringIndexOutOfBoundsException` with empty path lookup on wildcard paths, fixes [#209](https://github.com/metosin/reitit/issues/209) * Don't throw `StringIndexOutOfBoundsException` with empty path lookup on wildcard paths, fixes [#209](https://github.com/metosin/reitit/issues/209)

View file

@ -1,12 +1,30 @@
# Frontend basics # Frontend basics
* https://github.com/metosin/reitit/tree/master/examples/frontend Reitit frontend integration is built from multiple layers:
- Core functions with some additional browser oriented features
- [Browser integration](./browser.md) for attaching Reitit to hash-change or HTML
history events
- Stateful wrapper for easy use of history integration
- Optional [controller extension](./controllers.md)
## Core functions
`reitit.frontend` provides few useful functions wrapping core functions: `reitit.frontend` provides few useful functions wrapping core functions:
- `match-by-path` version which parses a URI using JavaScript, including `match-by-path` version which parses a URI using JavaScript, including
query-string, and also coerces the parameters. query-string, and also [coerces the parameters](../coercion/coercion.md).
- `router` which compiles coercers by default Coerced parameters are stored in match `:parameters` property. If coercion
- `match-by-name` and `match-by-name!` with optional `path-paramers` and is not enabled, the original parameters are stored in the same property,
logging errors to `console.warn` instead of throwing errors (to prevent to allow the same code to read parameters regardless if coercion is
React breaking due to errors). enabled.
`router` which compiles coercers by default.
`match-by-name` and `match-by-name!` with optional `path-paramers` and
logging errors to `console.warn` instead of throwing errors to prevent
React breaking due to errors.
## Next
[Browser integration](./browser.md)

View file

@ -21,5 +21,7 @@ Check examples for simple Ring handler example.
## Easy ## Easy
Reitit frontend routers require storing the state somewhere and passing it to Reitit frontend routers require storing the state somewhere and passing it to
all the calls. Wrapper (`reitit.frontend.easy`) is provided which manages all the calls. Wrapper `reitit.frontend.easy` is provided which manages
router instance and passes the instance to all calls. a router instance and passes the instance to all calls. This should
allow easy use in most applications, as browser anyway can only have single
event handler for page change events.

View file

@ -9,32 +9,34 @@ Controllers run code when a route is entered and left. This can be useful to:
## How controllers work ## How controllers work
A controller consists of three functions: A controller map can contain these properties:
* `params` which takes a Match and returns an arbitrary value. * `identity` function which takes a Match and returns an arbitrary value,
* `start` which takes the result of params and whose return value is discarded. * or `parameters` value, which declares which parameters should affect
* `stop` which takes the result of params and whose return value is discarded. controller identity
* `start` & `stop` functions, which are called with controller identity
When you navigate to a route that has a controller, `params` gets called first When you navigate to a route that has a controller, controller identity
and then `start` is called with its return value. When you exit that route, is first resolved by calling `identity` function, or by using `parameters`
`stop` is called with the return value of `params.` declaration, or if neither is set, the identity is `nil`. Next controller
is initialized by calling `start` is called with the identity value.
When you exit that route, `stop` is called with the return value of `params.`
If you navigate to the same route with different parameters, `params` gets If you navigate to the same route with different match, identity gets
called again. If the return value changes from the previous return value, `stop` resolved again. If the identity changes from the previous value, controller
and `start` get called again. is reinitialized: `stop` and `start` get called again.
You can add controllers to a route by adding them to the route data in the You can add controllers to a route by adding them to the route data in the
`:controllers` vector. For example: `:controllers` vector. For example:
```clojure ```cljs
["/item/:id" ["/item/:id"
{:controllers [{:params (fn [match] (get-in match [:path-params :id])) {:controllers [{:parameters {:path [:id]}
:start (fn [item-id] (js/console.log :start item-id)) :start (fn [parameters] (js/console.log :start (-> parameters :path :id)))
:stop (fn [item-id] (js/console.log :stop item-id))}]}] :stop (fn [parameters] (js/console.log :stop (-> parameters :path :id)))}]}]
``` ```
If you leave out `params`, `start` and `stop` get called with `nil`. You can You can leave out `start` or `stop` if you do not need both of them.
leave out `start` or `stop` if you do not need both of them.
## Enabling controllers ## Enabling controllers
@ -44,8 +46,7 @@ call
the URL changes. You can call it from the `on-navigate` callback of the URL changes. You can call it from the `on-navigate` callback of
`reitit.frontend.easy`: `reitit.frontend.easy`:
```clojure ```cljs
(ns frontend.core (ns frontend.core
(:require [reitit.frontend.easy :as rfe] (:require [reitit.frontend.easy :as rfe]
[reitit.frontend.controllers :as rfc])) [reitit.frontend.controllers :as rfc]))
@ -70,10 +71,10 @@ See also [the full example](https://github.com/metosin/reitit/tree/master/exampl
## Nested controllers ## Nested controllers
When you nest routes in the route tree, the controllers get nested as well. When you nest routes in the route tree, the controllers get concatenated when
Consider this route tree: route data is merged. Consider this route tree:
```clojure ```cljs
["/" {:controllers [{:start (fn [_] (js/console.log "root start"))}]} ["/" {:controllers [{:start (fn [_] (js/console.log "root start"))}]}
["/item/:id" ["/item/:id"
{:controllers [{:params (fn [match] (get-in match [:path-params :id])) {:controllers [{:params (fn [match] (get-in match [:path-params :id]))
@ -90,20 +91,22 @@ Consider this route tree:
started with the parameter `something-else`. The root controller stays on the started with the parameter `something-else`. The root controller stays on the
whole time since its parameters do not change. whole time since its parameters do not change.
## Authentication ## Tips
### Authentication
Controllers can be used to load resources from a server. If and when your Controllers can be used to load resources from a server. If and when your
API requires authentication you will need to implement logic to prevent controllers API requires authentication you will need to implement logic to prevent controllers
trying to do requests if user isn't authenticated yet. trying to do requests if user isn't authenticated yet.
### Run controllers and check authentication #### Run controllers and check authentication
If you have both unauthenticated and authenticated resources, you can If you have both unauthenticated and authenticated resources, you can
run the controllers always and then check the authentication status run the controllers always and then check the authentication status
on controller code, or on the code called from controllers (e.g. re-frame event on controller code, or on the code called from controllers (e.g. re-frame event
handler). handler).
### Disable controllers until user is authenticated #### Disable controllers until user is authenticated
If all your resources require authentication an easy way to prevent bad If all your resources require authentication an easy way to prevent bad
requests is to enable controllers only after authentication is done. requests is to enable controllers only after authentication is done.
@ -119,7 +122,6 @@ Similar solution could be used to describe required resources as data (maybe
even GraphQL query) per route, and then have code automatically load even GraphQL query) per route, and then have code automatically load
missing resources. missing resources.
## Controllers elsewhere ## Controllers elsewhere
* [Controllers in Keechma](https://keechma.com/guides/controllers/) * [Controllers in Keechma](https://keechma.com/guides/controllers/)

View file

@ -65,12 +65,11 @@
{:name ::item {:name ::item
:parameters {:path {:id s/Int} :parameters {:path {:id s/Int}
:query {(s/optional-key :foo) s/Keyword}} :query {(s/optional-key :foo) s/Keyword}}
:controllers [{:params (fn [match] :controllers [{:parameters {:path [:id]}
(:path (:parameters match))) :start (fn [{:keys [path]}]
:start (fn [params] (js/console.log "start" "item controller" (:id path)))
(js/console.log "start" "item controller" (:id params))) :stop (fn [{:keys [path]}]
:stop (fn [params] (js/console.log "stop" "item controller" (:id path)))}]}]]]
(js/console.log "stop" "item controller" (:id params)))}]}]]]
{:data {:controllers [{:start (log-fn "start" "root-controller") {:data {:controllers [{:start (log-fn "start" "root-controller")
:stop (log-fn "stop" "root controller")}] :stop (log-fn "stop" "root controller")}]
:coercion rsc/coercion}})) :coercion rsc/coercion}}))

View file

@ -1,29 +1,56 @@
(ns reitit.frontend.controllers) (ns reitit.frontend.controllers
"Provides apply-controllers function")
(defn- pad-same-length [a b] (defn- pad-same-length [a b]
(concat a (take (- (count b) (count a)) (repeat nil)))) (concat a (take (- (count b) (count a)) (repeat nil))))
(defn get-params (def ^:private params-warning
"Get controller parameters given match. If controller provides :params (delay (js/console.warn "Reitit-frontend controller :params is deprecated. Replace with :identity or :parameters option.")))
function that will be called with the match. Default is nil."
[controller match] (defn get-identity
(if-let [f (:params controller)] "Get controller identity given controller and match.
(f match)))
To select interesting properties from Match :parameters option can be set.
Value should be param-type => [param-key]
Resulting value is map of param-type => param-key => value.
For other uses, :identity option can be used to provide function from
Match to identity.
Default value is nil, i.e. controller identity doesn't depend on Match."
[{:keys [identity parameters params]} match]
(assert (not (and identity parameters))
"Use either :identity or :parameters for controller, not both.")
(when params
@params-warning)
(cond
parameters
(into {} (for [[param-type ks] parameters]
[param-type (select-keys (get (:parameters match) param-type) ks)]))
identity
(identity match)
;; Support deprecated :params for transition period. Can be removed later.
params
(params match)
:else nil))
(defn apply-controller (defn apply-controller
"Run side-effects (:start or :stop) for controller. "Run side-effects (:start or :stop) for controller.
The side-effect function is called with controller params." The side-effect function is called with controller identity value."
[controller method] [controller method]
(when-let [f (get controller method)] (when-let [f (get controller method)]
(f (::params controller)))) (f (::identity controller))))
(defn apply-controllers (defn apply-controllers
"Applies changes between current controllers and "Applies changes between current controllers and
those previously enabled. Resets controllers whose those previously enabled. Reinitializes controllers whose
parameters have changed." identity has changed."
[old-controllers new-match] [old-controllers new-match]
(let [new-controllers (mapv (fn [controller] (let [new-controllers (mapv (fn [controller]
(assoc controller ::params (get-params controller new-match))) (assoc controller ::identity (get-identity controller new-match)))
(:controllers (:data new-match))) (:controllers (:data new-match)))
changed-controllers (->> (map (fn [old new] changed-controllers (->> (map (fn [old new]
;; different controllers, or params changed ;; different controllers, or params changed
@ -33,7 +60,7 @@
(pad-same-length new-controllers old-controllers)) (pad-same-length new-controllers old-controllers))
(keep identity) (keep identity)
vec)] vec)]
(doseq [controller (map :old changed-controllers)] (doseq [controller (reverse (map :old changed-controllers))]
(apply-controller controller :stop)) (apply-controller controller :stop))
(doseq [controller (map :new changed-controllers)] (doseq [controller (map :new changed-controllers)]
(apply-controller controller :start)) (apply-controller controller :start))

View file

@ -1,4 +1,6 @@
(ns reitit.frontend.history (ns reitit.frontend.history
"Provides integration to hash-change or HTML5 History
events."
(:require [reitit.core :as reitit] (:require [reitit.core :as reitit]
[reitit.core :as r] [reitit.core :as r]
[reitit.frontend :as rf] [reitit.frontend :as rf]

View file

@ -15,15 +15,15 @@
:stop (fn [_] (swap! log conj :stop-2))} :stop (fn [_] (swap! log conj :stop-2))}
controller-3 {:start (fn [{:keys [foo]}] (swap! log conj [:start-3 foo])) controller-3 {:start (fn [{:keys [foo]}] (swap! log conj [:start-3 foo]))
:stop (fn [{:keys [foo]}] (swap! log conj [:stop-3 foo])) :stop (fn [{:keys [foo]}] (swap! log conj [:stop-3 foo]))
:params (fn [match] :identity (fn [match]
{:foo (-> match :parameters :path :foo)})}] {:foo (-> match :parameters :path :foo)})}]
(testing "single controller started" (testing "single controller started"
(swap! controller-state rfc/apply-controllers (swap! controller-state rfc/apply-controllers
{:data {:controllers [controller-1]}}) {:data {:controllers [controller-1]}})
(is (= [:start-1] @log)) (is (= [:start-1] @log))
(is (= [(assoc controller-1 ::rfc/params nil)] @controller-state)) (is (= [(assoc controller-1 ::rfc/identity nil)] @controller-state))
(reset! log [])) (reset! log []))
(testing "second controller started" (testing "second controller started"
@ -31,8 +31,8 @@
{:data {:controllers [controller-1 controller-2]}}) {:data {:controllers [controller-1 controller-2]}})
(is (= [:start-2] @log)) (is (= [:start-2] @log))
(is (= [(assoc controller-1 ::rfc/params nil) (is (= [(assoc controller-1 ::rfc/identity nil)
(assoc controller-2 ::rfc/params nil)] (assoc controller-2 ::rfc/identity nil)]
@controller-state)) @controller-state))
(reset! log [])) (reset! log []))
@ -42,8 +42,8 @@
:parameters {:path {:foo 5}}}) :parameters {:path {:foo 5}}})
(is (= [:stop-2 [:start-3 5]] @log)) (is (= [:stop-2 [:start-3 5]] @log))
(is (= [(assoc controller-1 ::rfc/params nil) (is (= [(assoc controller-1 ::rfc/identity nil)
(assoc controller-3 ::rfc/params {:foo 5})] (assoc controller-3 ::rfc/identity {:foo 5})]
@controller-state)) @controller-state))
(reset! log [])) (reset! log []))
@ -53,8 +53,8 @@
:parameters {:path {:foo 1}}}) :parameters {:path {:foo 1}}})
(is (= [[:stop-3 5] [:start-3 1]] @log)) (is (= [[:stop-3 5] [:start-3 1]] @log))
(is (= [(assoc controller-1 ::rfc/params nil) (is (= [(assoc controller-1 ::rfc/identity nil)
(assoc controller-3 ::rfc/params {:foo 1})] (assoc controller-3 ::rfc/identity {:foo 1})]
@controller-state)) @controller-state))
(reset! log [])) (reset! log []))
@ -62,7 +62,47 @@
(swap! controller-state rfc/apply-controllers (swap! controller-state rfc/apply-controllers
{:data {:controllers []}}) {:data {:controllers []}})
(is (= [:stop-1 [:stop-3 1]] @log)) (is (= [[:stop-3 1] :stop-1] @log))
(is (= [] @controller-state)) (is (= [] @controller-state))
(reset! log []))))
(deftest controller-data-parameters
(let [log (atom [])
controller-state (atom [])
static {:start (fn [params] (swap! log conj [:start-static]))
:stop (fn [params] (swap! log conj [:stop-static]))}
controller {:start (fn [params] (swap! log conj [:start params]))
:stop (fn [params] (swap! log conj [:stop params]))
:parameters {:path [:foo]}}]
(testing "init"
(swap! controller-state rfc/apply-controllers
{:data {:controllers [static controller]}
:parameters {:path {:foo 1}}})
(is (= [[:start-static]
[:start {:path {:foo 1}}]] @log))
(is (= [(assoc static ::rfc/identity nil)
(assoc controller ::rfc/identity {:path {:foo 1}})]
@controller-state))
(reset! log [])) (reset! log []))
))
(testing "params change"
(swap! controller-state rfc/apply-controllers
{:data {:controllers [static controller]}
:parameters {:path {:foo 5}}})
(is (= [[:stop {:path {:foo 1}}]
[:start {:path {:foo 5}}]] @log))
(is (= [(assoc static ::rfc/identity nil)
(assoc controller ::rfc/identity {:path {:foo 5}})]
@controller-state))
(reset! log []))
(testing "stop"
(swap! controller-state rfc/apply-controllers
{:data {:controllers []}})
(is (= [[:stop {:path {:foo 5}}]
[:stop-static]] @log))
(reset! log []))))