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)
* 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
* 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:
- `match-by-path` version which parses a URI using JavaScript, including
query-string, and also coerces the parameters.
- `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).
`match-by-path` version which parses a URI using JavaScript, including
query-string, and also [coerces the parameters](../coercion/coercion.md).
Coerced parameters are stored in match `:parameters` property. If coercion
is not enabled, the original parameters are stored in the same property,
to allow the same code to read parameters regardless if coercion is
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
Reitit frontend routers require storing the state somewhere and passing it to
all the calls. Wrapper (`reitit.frontend.easy`) is provided which manages
router instance and passes the instance to all calls.
all the calls. Wrapper `reitit.frontend.easy` is provided which manages
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
A controller consists of three functions:
A controller map can contain these properties:
* `params` which takes a Match and returns an arbitrary value.
* `start` which takes the result of params and whose return value is discarded.
* `stop` which takes the result of params and whose return value is discarded.
* `identity` function which takes a Match and returns an arbitrary value,
* or `parameters` value, which declares which parameters should affect
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
and then `start` is called with its return value. When you exit that route,
`stop` is called with the return value of `params.`
When you navigate to a route that has a controller, controller identity
is first resolved by calling `identity` function, or by using `parameters`
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
called again. If the return value changes from the previous return value, `stop`
and `start` get called again.
If you navigate to the same route with different match, identity gets
resolved again. If the identity changes from the previous value, controller
is reinitialized: `stop` and `start` get called again.
You can add controllers to a route by adding them to the route data in the
`:controllers` vector. For example:
```clojure
```cljs
["/item/:id"
{:controllers [{:params (fn [match] (get-in match [:path-params :id]))
:start (fn [item-id] (js/console.log :start item-id))
:stop (fn [item-id] (js/console.log :stop item-id))}]}]
{:controllers [{:parameters {:path [:id]}
:start (fn [parameters] (js/console.log :start (-> parameters :path :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
leave out `start` or `stop` if you do not need both of them.
You can leave out `start` or `stop` if you do not need both of them.
## Enabling controllers
@ -44,8 +46,7 @@ call
the URL changes. You can call it from the `on-navigate` callback of
`reitit.frontend.easy`:
```clojure
```cljs
(ns frontend.core
(:require [reitit.frontend.easy :as rfe]
[reitit.frontend.controllers :as rfc]))
@ -70,10 +71,10 @@ See also [the full example](https://github.com/metosin/reitit/tree/master/exampl
## Nested controllers
When you nest routes in the route tree, the controllers get nested as well.
Consider this route tree:
When you nest routes in the route tree, the controllers get concatenated when
route data is merged. Consider this route tree:
```clojure
```cljs
["/" {:controllers [{:start (fn [_] (js/console.log "root start"))}]}
["/item/: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
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
API requires authentication you will need to implement logic to prevent controllers
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
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
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
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
missing resources.
## Controllers elsewhere
* [Controllers in Keechma](https://keechma.com/guides/controllers/)

View file

@ -65,12 +65,11 @@
{:name ::item
:parameters {:path {:id s/Int}
:query {(s/optional-key :foo) s/Keyword}}
:controllers [{:params (fn [match]
(:path (:parameters match)))
:start (fn [params]
(js/console.log "start" "item controller" (:id params)))
:stop (fn [params]
(js/console.log "stop" "item controller" (:id params)))}]}]]]
:controllers [{:parameters {:path [:id]}
:start (fn [{:keys [path]}]
(js/console.log "start" "item controller" (:id path)))
:stop (fn [{:keys [path]}]
(js/console.log "stop" "item controller" (:id path)))}]}]]]
{:data {:controllers [{:start (log-fn "start" "root-controller")
:stop (log-fn "stop" "root controller")}]
: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]
(concat a (take (- (count b) (count a)) (repeat nil))))
(defn get-params
"Get controller parameters given match. If controller provides :params
function that will be called with the match. Default is nil."
[controller match]
(if-let [f (:params controller)]
(f match)))
(def ^:private params-warning
(delay (js/console.warn "Reitit-frontend controller :params is deprecated. Replace with :identity or :parameters option.")))
(defn get-identity
"Get controller identity given controller and 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
"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]
(when-let [f (get controller method)]
(f (::params controller))))
(f (::identity controller))))
(defn apply-controllers
"Applies changes between current controllers and
those previously enabled. Resets controllers whose
parameters have changed."
those previously enabled. Reinitializes controllers whose
identity has changed."
[old-controllers new-match]
(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)))
changed-controllers (->> (map (fn [old new]
;; different controllers, or params changed
@ -33,7 +60,7 @@
(pad-same-length new-controllers old-controllers))
(keep identity)
vec)]
(doseq [controller (map :old changed-controllers)]
(doseq [controller (reverse (map :old changed-controllers))]
(apply-controller controller :stop))
(doseq [controller (map :new changed-controllers)]
(apply-controller controller :start))

View file

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

View file

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