Merge branch 'develop' into re-frame-tweaks
# Conflicts: # src/re_frame/std_interceptors.cljc
This commit is contained in:
commit
a5d3c6020f
|
@ -1,5 +1,4 @@
|
|||
![logo](/images/logo/re-frame_256w.png?raw=true)
|
||||
|
||||
![logo](/images/logo/re-frame_512w.png?raw=true)
|
||||
|
||||
## Derived Values, Flowing
|
||||
|
||||
|
@ -16,7 +15,7 @@ y'know. Pretty good.
|
|||
Either:
|
||||
|
||||
1. You want to develop an [SPA] in ClojureScript, and you are looking for a framework; or
|
||||
2. You believe that, by early 2015, ReactJS had won the JavaScript framework wars and
|
||||
2. You believe that, by early 2015, React had won the JavaScript framework wars and
|
||||
you are curious about the bigger implications. Is the combination of
|
||||
`reactive programming`, `functional programming` and `immutable data` going to
|
||||
**completely change everything**? And, if so, what would that look like in a language
|
||||
|
@ -109,7 +108,7 @@ __Warning__: That was the summary. What follows is a long-ish tutorial/explanat
|
|||
First, we decided to build our SPA apps with ClojureScript, then we
|
||||
choose [Reagent], then we had a problem.
|
||||
|
||||
For all its considerable brilliance, Reagent (+ ReactJS)
|
||||
For all its considerable brilliance, Reagent (+ React)
|
||||
delivers only the 'V' part of a traditional MVC framework.
|
||||
|
||||
But apps involve much more than V. Where
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
## Simpler Apps
|
||||
|
||||
To build a re-frame app, you:
|
||||
- design your app's data structure (data layer)
|
||||
- write and register subscription functions (query layer)
|
||||
- write Reagent component functions (view layer)
|
||||
- write and register event handler functions (control layer and/or state transition layer)
|
||||
|
||||
For simpler apps, you should put code for each layer into separate files:
|
||||
```
|
||||
src
|
||||
├── core.cljs <--- entry point, plus history, routing, etc
|
||||
├── db.cljs <--- schema, validation, etc (data layer)
|
||||
├── subs.cljs <--- subscription handlers (query layer)
|
||||
├── views.cljs <--- reagent components (view layer)
|
||||
└── events.cljs <--- event handlers (control/update layer)
|
||||
```
|
||||
|
||||
For a living example of this approach, look at the [todomvc example](https://github.com/Day8/re-frame/tree/master/examples/todomvc).
|
||||
|
||||
### There's A Small Gotcha
|
||||
|
||||
If you adopt this structure there's a gotcha.
|
||||
|
||||
`events.cljs` and `subs.cljs` will never be `required` by any other
|
||||
namespaces. To the Google Closure dependency mechanism it appears as
|
||||
if these two namespaces are not needed and it doesn't load them.
|
||||
|
||||
And, if the code does not get loaded, the registrations in these namespaces
|
||||
never happen. You'll then be baffled as to why none of your events handlers
|
||||
are registered.
|
||||
|
||||
Once you twig to what's going on, the solution is easy. You must
|
||||
explicitly `require` both namespaces, `events` and `subs`, in your `core`
|
||||
namespace. Then they'll be loaded and the registrations will occur
|
||||
as that loading happens.
|
||||
|
||||
## Larger Apps
|
||||
|
||||
Assuming your larger apps has multiple "panels" (or "views") which are
|
||||
relatively independent, you might use this structure:
|
||||
```
|
||||
src
|
||||
├── panel-1
|
||||
│ ├── db.cljs <--- schema, validation, etc (data layer)
|
||||
│ ├── subs.cljs <--- subscription handlers (query layer)
|
||||
│ ├── views.cljs <--- reagent components (view layer)
|
||||
│ └── events.cljs <--- event handlers (control/update layer)
|
||||
├── panel-2
|
||||
│ ├── db.cljs <--- schema, validation. etc (data layer)
|
||||
│ ├── subs.cljs <--- subscription handlers (query layer)
|
||||
│ ├── views.cljs <--- reagent components (view layer)
|
||||
│ └── events.cljs <--- event handlers (control/update layer)
|
||||
.
|
||||
.
|
||||
└── panel-n
|
||||
```
|
||||
|
||||
Continue to [Navigation](Navigation.md) to learn how to switch between panels of a larger app.
|
|
@ -298,7 +298,7 @@ More on side effects in a minute, but let's double back to coeffects.
|
|||
|
||||
### The Coeffects
|
||||
|
||||
So far we've written our new style `-fx handlers like this:
|
||||
So far we've written our new style `-fx` handlers like this:
|
||||
```clj
|
||||
(reg-event-fx
|
||||
:my-event
|
||||
|
@ -314,10 +314,11 @@ It is now time to name that first argument:
|
|||
{ ... }))
|
||||
```
|
||||
|
||||
When you use the `-fx` form of registration, the first argument of your handler will be a map of coeffects which we name `cofx`.
|
||||
When you use the `-fx` form of registration, the first argument
|
||||
of your handler will be a map of coeffects which we name `cofx`.
|
||||
|
||||
In that map will be the complete set of "inputs" required by your function. The complete
|
||||
set of computational resources (data) needed to perform its computation. But how?
|
||||
set of computational resources (data) needed to perform its computation. But how?
|
||||
This will be explained in an upcoming tutorial, I promise, but for the moment,
|
||||
take it as a magical given.
|
||||
|
||||
|
@ -332,7 +333,8 @@ Remember this impure handler from before:
|
|||
(assoc db :defaults defaults))))
|
||||
```
|
||||
|
||||
We'd now rewrite that as a pure handler, like this:
|
||||
It was impure because it obtained an input from other than its arguments.
|
||||
We'd now rewrite it as a pure handler, like this:
|
||||
```clj
|
||||
(reg-event-fx ;; notice the -fx
|
||||
:load-localstore
|
||||
|
@ -389,3 +391,8 @@ cause additional side-effects (effects). That's when you reach for `-fx` handle
|
|||
In the next tutorial, we'll shine a light on `interceptors` which are
|
||||
the mechanism by which event handlers are executed. That knowledge will give us a springboard
|
||||
to then, as a next step, better understand coeffects and effects. We'll soon be writing our own.
|
||||
|
||||
---
|
||||
Up: [Index](Readme.md)
|
||||
Next: [Interceptors](Interceptors.md)
|
||||
|
|
@ -266,19 +266,29 @@ Want to stub out the `:dispatch` effect? Do this:
|
|||
If your test does alter registered effect handlers, and you are using `cljs.test`,
|
||||
then you can use a `fixture` to restore all effect handlers at the end of your test:
|
||||
```clj
|
||||
(defn re-frame-fixture
|
||||
[f]
|
||||
(let [restore-re-frame-fn (re-frame.core/make-restore-fn)]
|
||||
(try
|
||||
(f)
|
||||
(finally (restore-re-frame-fn)))))
|
||||
|
||||
(cljs.test/use-fixtures :each re-frame-fixture)
|
||||
(defn fixture-re-frame
|
||||
[]
|
||||
(let [restore-re-frame (atom nil)]
|
||||
{:before #(reset! restore-re-frame (re-frame.core/make-restore-fn))
|
||||
:after #(@restore-re-frame)}))
|
||||
|
||||
(use-fixtures :each (fixture-re-frame))
|
||||
```
|
||||
|
||||
`re-frame.core/make-restore-fn` creates a checkpoint for re-frame state (including
|
||||
registered handlers) to which you can return.
|
||||
|
||||
### Summary
|
||||
|
||||
XXX
|
||||
|
||||
|
||||
---
|
||||
Previous: [Interceptors](Interceptors.md)
|
||||
Up: [Index](Readme.md)
|
||||
Next: [Coeffects](Coeffects.md)
|
||||
|
||||
---
|
||||
|
||||
### Builtin Effect Handlers
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ and inserts its own interceptors
|
|||
so ACTUALLY, there's about 5 interceptors in the chain.
|
||||
|
||||
So, ultimately, that event registration associates the event id `:some-id`
|
||||
with a chain of interceptors.
|
||||
with __just__ a chain of interceptors. Nothing more.
|
||||
|
||||
Later, when a `(dispatch [:some-id ...])` happens, that 5-chain of
|
||||
interceptors will be "executed". And that's how events get handled.
|
||||
|
@ -216,7 +216,7 @@ designed by the talented
|
|||
|
||||
Dunno about you, but I'm easily offended by underscores.
|
||||
|
||||
If our components did this:
|
||||
If we had a component which did this:
|
||||
```clj
|
||||
(dispatch [:delete-item 42])
|
||||
```
|
||||
|
@ -252,7 +252,7 @@ changing the `:event` value within the `:coeffects`.
|
|||
|
||||
`:event` will start off as `[:delete-item 42]`, but will end up `[42]`. `trim-event` will remove that
|
||||
leading `:delete-item` because, by the time the event is
|
||||
being processed, we already know what id is has.
|
||||
being processed, we already know what id it has.
|
||||
|
||||
And, here it is:
|
||||
```clj
|
||||
|
@ -337,6 +337,11 @@ __4.__ Interceptors do interesting things:
|
|||
|
||||
In the next Tutorial, we'll look at (side) Effects in more depth. Later again, we'll look at Coeffects.
|
||||
|
||||
---
|
||||
Previous: [Interceptors](Interceptors.md)
|
||||
Up: [Index](Readme.md)
|
||||
Next: [Effects](Effects.md)
|
||||
|
||||
|
||||
## Appendix
|
||||
|
||||
|
|
|
@ -0,0 +1,223 @@
|
|||
## Bootstrapping Application State
|
||||
|
||||
To bootstrap a re-frame application, you need to:
|
||||
1. register handlers
|
||||
- subscription (via `reg-sub`)
|
||||
- events (via `reg-event-db` or `reg-event-fx`)
|
||||
- effects (via `reg-fx`)
|
||||
- coeffects (via `reg-cofx`)
|
||||
2. kickstart reagent (views)
|
||||
3. Load the right initial data into `app-db` which might be a `merge` of:
|
||||
- Some default values
|
||||
- Values stored in LocalStorage
|
||||
- Values obtained via service calls to server
|
||||
- etc, etc
|
||||
|
||||
Point 3 is the interesting bit and will be the main focus of this page, but let's work our way through them ...
|
||||
|
||||
## 1. Register Handlers
|
||||
|
||||
re-frame's multifarious handlers all work in the same way. You declare
|
||||
and registered your handlers in the one step, like this "event handler" example:
|
||||
```clj
|
||||
(re-frame/reg-event-db ;; event handler will be registered automatically
|
||||
:some-id
|
||||
(fn [db [_ value]]
|
||||
... do some state change based on db and value ))
|
||||
```
|
||||
|
||||
As a result, there's nothing further you need to do because
|
||||
handler registration happens as a direct result of loading the code
|
||||
code (presumably via a `<script>`).
|
||||
|
||||
|
||||
## 2. Kick Start Reagent
|
||||
|
||||
Create a function `main` which does a `reagent/render` of your root reagent component `main-panel`:
|
||||
|
||||
```clj
|
||||
(defn main-panel ;; my top level reagent component
|
||||
[]
|
||||
[:div "Hello DDATWD"])
|
||||
|
||||
(defn ^:export main ;; call this to bootstrap your app
|
||||
[]
|
||||
(reagent/render [main-panel]
|
||||
(js/document.getElementById "app")))
|
||||
```
|
||||
|
||||
Mounting the top level component `main-panel` will trigger a cascade of child
|
||||
component creation. The full DOM tree will be rendered.
|
||||
|
||||
## 3. Loading Initial Data
|
||||
|
||||
Let's rewrite our `main-panel` component to use a subscription. In effect,
|
||||
we want it to source and render some data held in `app-db`.
|
||||
|
||||
First, we'll create the subscription handler:
|
||||
```Clojure
|
||||
(re-frame/reg-sub ;; a new subscription handler
|
||||
:name ;; usage (subscribe [:name])
|
||||
(fn [db _]
|
||||
(:display-name db))) ;; extracts `:display-name` from app-db
|
||||
```
|
||||
|
||||
And now we use that subscription:
|
||||
```clj
|
||||
(defn main-panel
|
||||
[]
|
||||
(let [name (re-frame/subscribe [:name])] ;; <--- a subscription <---
|
||||
(fn []
|
||||
[:div "Hello " @name])))) ;; <--- use the result of the subscription
|
||||
```
|
||||
|
||||
The user of our app will see funny things
|
||||
if that `(subscribe [:name])` doesn't deliver good data. But how do we ensure "good data"?
|
||||
|
||||
That will require:
|
||||
1. getting data into `app-db`; and
|
||||
2. not get into trouble if that data isn't yet in `app-db`. For example,
|
||||
the data may have to come from a server and there's latency.
|
||||
|
||||
**Note: `app-db` initially contains `{}`**
|
||||
|
||||
### Getting Data Into `app-db`
|
||||
|
||||
Only event handlers can change `app-db`. Those are the rules!! Indeed, even initial
|
||||
values must be put in `app-db` via an event handler.
|
||||
|
||||
Here's an event handler for that purpose:
|
||||
```Clojure
|
||||
(re-frame/reg-event-db
|
||||
:initialise-db ;; usage: (dispatch [:initialise-db])
|
||||
(fn [_ _] ;; Ignore both params (db and event)
|
||||
{:display-name "DDATWD" ;; return a new value for app-db
|
||||
:items [1 2 3 4]}))
|
||||
```
|
||||
|
||||
You'll notice that this handler does nothing other than to return a ` map`. That map
|
||||
will become the new value within `app-db`.
|
||||
|
||||
We'll need to dispatch an `:initialise-db` event to get it to execute. `main` seems like the natural place:
|
||||
```Clojure
|
||||
(defn ^:export main
|
||||
[]
|
||||
(re-frame/dispatch [:initialise-db]) ;; <--- this is new
|
||||
(reagent/render [main-panel]
|
||||
(js/document.getElementById "app")))
|
||||
```
|
||||
|
||||
But remember, event handlers execute async. So although there's
|
||||
a `dispatch` within `main`, the event is simply queued, and the
|
||||
handler for `:initialise-db`
|
||||
will not be run until sometime after `main` has finished.
|
||||
|
||||
But how long after? And is there a race condition? The
|
||||
component `main-panel` (which assumes good data) might be
|
||||
rendered before the `:initialise-db` event handler has
|
||||
put good data into `app-db`.
|
||||
|
||||
We don't want any rendering (of `main-panel`) until after `app-db`
|
||||
has been correctly initialised.
|
||||
|
||||
Okay, so that's enough of teasing-out the issues. Let's see a
|
||||
quick sketch of the entire pattern. It is very straight-forward.
|
||||
|
||||
## The Pattern
|
||||
|
||||
```Clojure
|
||||
(re-frame/reg-sub ;; the means by which main-panel gets data
|
||||
:name ;; usage (subscribe [:name])
|
||||
(fn [db _]
|
||||
(:display-name db)))
|
||||
|
||||
(re-frame/reg-sub ;; we can check if there is data
|
||||
:initialised? ;; usage (subscribe [:initialised?])
|
||||
(fn [db _]
|
||||
(not (empty? db)))) ;; do we have data
|
||||
|
||||
(defn main-panel ;; the top level of our app
|
||||
[]
|
||||
(let [name (re-frame/subscribe :name)] ;; we need there to be good data
|
||||
(fn []
|
||||
[:div "Hello " @name]))))
|
||||
|
||||
(defn top-panel ;; this is new
|
||||
[]
|
||||
(let [ready? (re-frame/subscribe [:initialised?])]
|
||||
(fn []
|
||||
(if-not @ready? ;; do we have good data?
|
||||
[:div "Initialising ..."] ;; tell them we are working on it
|
||||
[main-panel])))) ;; all good, render this component
|
||||
|
||||
(defn ^:export main ;; call this to bootstrap your app
|
||||
[]
|
||||
(re-frame/dispatch [:initialise-db])
|
||||
(reagent/render [top-panel]
|
||||
(js/document.getElementById "app")))
|
||||
```
|
||||
|
||||
## Scales Up
|
||||
|
||||
This pattern scales up easily.
|
||||
|
||||
For example, imagine a more complicated scenario in which your app
|
||||
is not fully initialised until 2 backend services supply data.
|
||||
|
||||
Your `main` might look like this:
|
||||
```Clojure
|
||||
(defn ^:export main ;; call this to bootstrap your app
|
||||
[]
|
||||
(re-frame/dispatch [:initialise-db]) ;; basics
|
||||
(re-frame/dispatch [:load-from-service-1]) ;; ask for data from service-1
|
||||
(re-frame/dispatch [:load-from-service-2]) ;; ask for data from service-2
|
||||
(reagent/render [top-panel]
|
||||
(js/document.getElementById "app")))
|
||||
```
|
||||
|
||||
Your `:initialised?` test then becomes more like this sketch:
|
||||
|
||||
```Clojure
|
||||
(reg-sub
|
||||
:initialised? ;; usage (subscribe [:initialised?])
|
||||
(fn [db _]
|
||||
(and (not (empty? db))
|
||||
(:service1-answered? db)
|
||||
(:service2-answered? db)))))
|
||||
```
|
||||
|
||||
This assumes boolean flags are set in `app-db` when data was loaded from these services.
|
||||
|
||||
## Cheating - Synchronous Dispatch
|
||||
|
||||
In simple cases, you can simplify matters by using `(dispatch-sync [:initialise-db])` in
|
||||
the main entry point function. The
|
||||
[Simple Example](https://github.com/Day8/re-frame/blob/8cf42f57f50f3ee41e74de1754fdb75f80b31775/examples/simple/src/simpleexample/core.cljs#L110)
|
||||
and [TodoMVC Example](https://github.com/Day8/re-frame/blob/8cf42f57f50f3ee41e74de1754fdb75f80b31775/examples/todomvc/src/todomvc/core.cljs#L35)
|
||||
both use `dispatch-sync` to initialise the app-db.
|
||||
|
||||
`dispatch` queues an event for later processing, but `dispatch-sync` acts
|
||||
like a function call and handles an event immediately. That's useful for initial data
|
||||
load we are considering, particularly for simple apps. Using `dispatch-sync` guarantees
|
||||
that initial state will be in place before any views are mounted, so we know they'll
|
||||
subscribe to sensible values. We don't need a guard like `top-panel` (introduced above).
|
||||
|
||||
But don't get into the habit of using `dispatch-sync` everywhere. It is the right
|
||||
tool in this context and, sometimes, when writing tests, but
|
||||
`dispatch` is the staple you should use everywhere else.
|
||||
|
||||
## Loading Initial Data From Services
|
||||
|
||||
Above, in our example `main`, we imagined using `(re-frame/dispatch [:load-from-service-1])` to request data
|
||||
from a backend services. How would we write the handler for this event?
|
||||
|
||||
The next Tutorial will show you how.
|
||||
|
||||
|
||||
|
||||
---
|
||||
Previous: [Interceptors](Interceptors.md)
|
||||
Up: [Index](Readme.md)
|
||||
Next: [Talking To Servers](Talking-To-Servers.md)
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
## Namespaced Ids
|
||||
|
||||
As an app gets bigger, you'll tend to get clashes on ids - event-ids, or query-ids (subscriptions), etc.
|
||||
|
||||
One panel will need to `dispatch` an `:edit` event and so will
|
||||
another, but the two panels will have different handlers.
|
||||
So how then to not have a clash? How then to distinguish between
|
||||
one `:edit` event and another?
|
||||
|
||||
Your goal should be to use event-ids which encode both the event
|
||||
itself (`:edit` ?) and the context (`:panel1` or `:panel2` ?).
|
||||
|
||||
Luckily, ClojureScript provides a nice easy solution: use keywords
|
||||
with a __synthetic namespace__. Perhaps something like `:panel1/edit` and `:panel2/edit`.
|
||||
|
||||
You see, ClojureScript allows the namespace in a keyword to be a total
|
||||
fiction. I can have the keyword `:panel1/edit` even though
|
||||
`panel1.cljs` doesn't exist.
|
||||
|
||||
Naturally, you'll take advantage of this by using keyword namespaces
|
||||
which are both unique and descriptive.
|
|
@ -0,0 +1,49 @@
|
|||
|
||||
## What About Navigation?
|
||||
|
||||
How do I switch between different panels of a larger app?
|
||||
|
||||
Your `app-db` could have an `:active-panel` key containing an id for the panel being displayed.
|
||||
|
||||
|
||||
When the user does something navigation-ish (selects a tab, a dropdown or something which changes the active panel), then the associated event and dispatch look like this:
|
||||
|
||||
```clj
|
||||
(re-frame/reg-event-db
|
||||
:set-active-panel
|
||||
(fn [db [_ value]]
|
||||
(assoc db :active-panel value)))
|
||||
|
||||
(re-frame/dispatch
|
||||
[:set-active-panel :panel1])
|
||||
```
|
||||
|
||||
A high level reagent view has a subscription to :active-panel and will switch to the associated panel.
|
||||
```clj
|
||||
(re-frame/reg-sub
|
||||
:active-panel
|
||||
(fn [db _]
|
||||
(:active-panel db)))
|
||||
|
||||
(defn panel1
|
||||
[]
|
||||
[:div {:on-click #(re-frame/dispatch [:set-active-panel :panel2])}
|
||||
"Here" ])
|
||||
|
||||
(defn panel2
|
||||
[]
|
||||
[:div "There"])
|
||||
|
||||
(defn high-level-view
|
||||
[]
|
||||
(let [active (re-frame/subscribe [:active-panel])]
|
||||
(fn []
|
||||
[:div
|
||||
[:div.title "Heading"]
|
||||
(condp = @active ;; or you could look up in a map
|
||||
:panel1 [panel1]
|
||||
:panel2 [panel2])])))
|
||||
|
||||
```
|
||||
|
||||
Continue to [Namespaced Keywords](Namespaced-Keywords.md) to reduce clashes on ids.
|
|
@ -1,8 +1,21 @@
|
|||
## Understanding Event Handlers:
|
||||
|
||||
Understanding Event Handlers:
|
||||
1. [Pure Event Handlers] TODO - write something good here
|
||||
2. [Effectful Handlers](EffectfulHandlers.md)
|
||||
3. [Interceptors](Interceptors.md)
|
||||
4. [Effects](Effects.md)
|
||||
5. [CoEffects](coeffects.md)
|
||||
|
||||
1. [EffectfulEvents](EffectfulEvents.md)
|
||||
2. [Interceptors](Interceptors.md)
|
||||
3. [Effects](Effects.md)
|
||||
4. [CoEffects](coeffects.md)
|
||||
|
||||
## Structuring Your Application:
|
||||
|
||||
1. [Basic App Structure](Basic-App-Structure.md)
|
||||
2. [Navigation](Navigation.md)
|
||||
3. [Namespaced Keywords](Namespaced-Keywords.md)
|
||||
|
||||
|
||||
## Populating Your Application Data:
|
||||
|
||||
1. [Loading Initial Data](Loading-Initial-Data.md)
|
||||
2. [Talking To Servers](Talking-To-Servers.md)
|
||||
3. [Subscribing to External Data](Subscribing-To-External-Data.md)
|
|
@ -0,0 +1,238 @@
|
|||
## Subscribing to External Data
|
||||
|
||||
In [Talking To Servers](Talking-To-Servers.md) we learned how to
|
||||
communicate with servers using both pure and effectful handlers.
|
||||
This is great, but what if you want to
|
||||
query external data using subscriptions the
|
||||
same way you query data stored in `app-db`? This tutorial will show you how.
|
||||
|
||||
### There Can Be Only One!!
|
||||
|
||||
`re-frame` apps have a single source of data called `app-db`.
|
||||
|
||||
The `re-frame` README asks you to imagine `app-db` as something of an in-memory database. You
|
||||
query it (via subscriptions) and transactionally update it (via event handlers).
|
||||
|
||||
### Components Don't Know, Don't Care
|
||||
|
||||
Components never know the structure of your `app-db`, much less its existence.
|
||||
|
||||
Instead, they `subscribe`, declaratively, to
|
||||
data, like this `(subscribe [:something "blah"])`, and that allows Components to
|
||||
obtain a stream of updates to "something", while knowing nothing about the source of the data.
|
||||
|
||||
### A 2nd Source
|
||||
|
||||
All good but ... SPAs are seldom completely self contained data-wise.
|
||||
|
||||
There's a continuum between apps which are 100% standalone data-wise,
|
||||
and those where remote data is utterly central to the app's function.
|
||||
In this page, we're exploring the remote-data-centric end of this continuum.
|
||||
|
||||
And just to be clear, when I'm talking about remote data, I'm thinking of data
|
||||
luxuriating in remote databases like firebase, rethinkdb, PostgreSQL, Datomic, etc
|
||||
- data sources that an app must query and mutate.
|
||||
|
||||
So, the question is: how would we integrate this kind of remote data into an app when
|
||||
re-frame seems to have only one source of data: `app-db`?
|
||||
How do we introduce a second or even third source of data? How should we `subscribe`
|
||||
to this remote data, and how would we `update` it?
|
||||
|
||||
By way of explanation, let's make the question specific: how could we wire up a
|
||||
Component which displays a collection of `items`,
|
||||
when those items come from a remote database?
|
||||
|
||||
In your mind's eye, imagine this kind of query against that remote database:
|
||||
`select id, price, description from items where type="see through"`.
|
||||
|
||||
### Via A Subscription
|
||||
|
||||
In `re-frame`, Components always obtain data via a subscription. Always.
|
||||
|
||||
So, our Component which shows items is going to
|
||||
```clj
|
||||
(let [items (re-frame/subscribe [:items "see through"]) ...
|
||||
```
|
||||
and the subscription handler will deliver them.
|
||||
|
||||
Which, in turn, means our code must have a subscription handler defined:
|
||||
```clj
|
||||
(re-frame/reg-sub
|
||||
:items
|
||||
(fn [db [_ item-type]
|
||||
...))
|
||||
```
|
||||
|
||||
Which is fine ... except we haven't really solved this problem yet, have we?
|
||||
We've just transferred
|
||||
the problem away from the Component and into the subscription handler?
|
||||
|
||||
Well, yes, we have, and isn't that a fine thing!! That's precisely what we want
|
||||
from our
|
||||
subscription handlers ... to manage how the data is sourced ... to hide that from
|
||||
the Component.
|
||||
|
||||
### The Subscription Handler's Job
|
||||
|
||||
Right, so let's write the subscription handler.
|
||||
|
||||
There'll be code in a minute but, first, let's describe how the subscription handler
|
||||
will work:
|
||||
|
||||
1. Upon being required to provide items, it has to issue
|
||||
a query to the remote database. Perhaps this will be done via a
|
||||
a RESTful GET. Or via a firebase connection. Or by pushing a JSON
|
||||
representation of the query down a websocket. Something. And it is the
|
||||
subscription handler's job to know how it is done.
|
||||
|
||||
2. This query be async - with the results arriving sometime "later". And when they
|
||||
eventually arrive, the handler must organise for the query results to be placed into `app-db`,
|
||||
at some known, particular path. In the meantime, the handler might want to ensure that the absence of
|
||||
results is also communicated to the Component, allowing it to display "Loading ...".
|
||||
[The Nine States of Design](https://medium.com/swlh/the-nine-states-of-design-5bfe9b3d6d85#.j52018nod)
|
||||
has some useful information on designing your application for different states that your data might be in.
|
||||
|
||||
3. The subscription handler must return something to the Component. It should give back a
|
||||
`reaction` to that known, particular path within `app-db`, so that when the query results
|
||||
eventually arrive, they will flow through into the Component for display.
|
||||
|
||||
4. The subscription handler will detect when the Component is destroyed and no longer requires
|
||||
the subscription. It will then clean up, getting rid of those now-unneeded items, and
|
||||
sorting out any stateful database connection issues.
|
||||
|
||||
Notice what's happening here. In many respects, `app-db` is still acting as the single source of data.
|
||||
The subscription handler is organising for the right remote data to "flow" into `app-db` at a known,
|
||||
particular path, when it is needed by a Component. And, equally, for this data to be cleaned up when it
|
||||
is no longer required.
|
||||
|
||||
### Some Code
|
||||
|
||||
Enough fluffing about with words, here's a code sketch for our subscription handler:
|
||||
```clj
|
||||
(re-frame/reg-sub-raw
|
||||
:items
|
||||
(fn [db [_ type]]
|
||||
(let [query-token (issue-items-query!
|
||||
type
|
||||
:on-success #(re-frame/dispatch [:write-to [:some :path]]))]
|
||||
(reagent/make-reaction
|
||||
(fn [] (get-in @db [:some :path] []))
|
||||
:on-dispose #(do (terminate-items-query! query-token)
|
||||
(re-frame/dispatch [:cleanup [:some :path]]))))))
|
||||
```
|
||||
|
||||
A few things to notice:
|
||||
|
||||
1. We are using the low level `reg-sub-raw` registration for our handler (and not `reg-sub`).
|
||||
This gives us some low level control. `db` will be an atom. We must return a
|
||||
`reaction` (signal).
|
||||
|
||||
2. You have to write `issue-items-query!`. Are you making a Restful GET?
|
||||
Are you writing JSON packets down a websocket? The query has to be made.
|
||||
|
||||
3. We do not issue the query via a `dispatch` because, to me, it isn't an event. But we most certainly
|
||||
do handle the arrival of query results via a `dispatch` and associated event handler. That to me
|
||||
is an external event happening to the system. The event handler can curate the arriving data in
|
||||
whatever way makes sense. Maybe it does nothing more than to `assoc` into an `app-db` path,
|
||||
or maybe this is a rethinkdb changefeed subscription and your event handler will have to collate
|
||||
the newly arriving data with what has previously been returned. Do what
|
||||
needs to be done in that event handler, so that the right data to be put into the right path.
|
||||
|
||||
3. We use Reagent's `make-reaction` function to create a reaction which will return
|
||||
that known, particular path within `app-db` where the query results are to be placed.
|
||||
|
||||
4. We use the `on-dispose` callback on this reaction to do any cleanup work
|
||||
when the subscription is no longer needed. Clean up `app-db`? Clean up the database connection?
|
||||
|
||||
### Any Good?
|
||||
|
||||
It turns out that this is a surprisingly flexible and clean approach. And pretty damn obvious once
|
||||
someone points it out to you (which is a good sign). There's a lot to like about it.
|
||||
|
||||
For example, if you are using rethinkdb, which supports queries which yield "change feeds" over time,
|
||||
rather than a one-off query result, you have to actively close such queries when they are no longer needed.
|
||||
That's easy to do in our cleanup code.
|
||||
|
||||
We can source some data from both PostgreSQL and firebase in the one app, using the same pattern.
|
||||
All remote data access is done in the same way.
|
||||
|
||||
Because query results are `dispatched` to an event handler, you have a lot of flexibility
|
||||
about how you process them.
|
||||
|
||||
The whole set of pieces can be arranged and tweaked in many ways. For example,
|
||||
with a bit of work, we could keep a register of all currently used queries.
|
||||
And then, if ever we noticed that the app had gone offline,
|
||||
and then back online, we could organise to reissue all the queries again
|
||||
(with results flowing back into
|
||||
the same known paths), avoiding stale results.
|
||||
|
||||
Also, notice that putting ALL interesting data into `app-db` has nice
|
||||
flow on effects. In particular, it means it is available to event handlers,
|
||||
should they need it when servicing events (event handlers get `db` as a parameter, right?).
|
||||
If this item data was held in a separate place, other than `app-db`,
|
||||
it wouldn't be available in this useful way.
|
||||
|
||||
### Warning: Undo/Redo
|
||||
|
||||
This technique caches remote data in `app-db`. Be sure to exclude this
|
||||
cache area from any undo/redo operations
|
||||
using [the available configuration options](https://github.com/Day8/re-frame-undo#harvesting-and-re-instating)
|
||||
|
||||
### Query De-duplication
|
||||
|
||||
In v0.8.0 of re-frame onwards, subscriptions are automatically de-duplicated.
|
||||
|
||||
In prior versions, in cases where the same query is simultaneously issued
|
||||
from multiple places, you'd want to
|
||||
de-duplicate the queries. One possibility is to do this duplication
|
||||
in `issue-items-query!` itself. You can count
|
||||
`count` the duplicate queries and only clear the data when that count goes to 0.
|
||||
|
||||
### Thanks To
|
||||
|
||||
@nidu for his valuable review comments and insights
|
||||
|
||||
## The Alternative Approach
|
||||
|
||||
Event handlers do most of the heavy lifting within re-frame apps.
|
||||
|
||||
When buttons gets clicked, or items get dragged 'n dropped, or tabs get
|
||||
chosen, they know how to transition the app from one state
|
||||
to the next. That's their job. And, when they make such
|
||||
a transition, it is quite reasonable to expect them to ALSO
|
||||
source the data needed in the new state.
|
||||
|
||||
So there's definitely a case for NOT using the approach outlined
|
||||
above and, instead, making event handlers source data and
|
||||
plonk it into a certain part of `app-db` for use by subscriptions.
|
||||
|
||||
In effect, there's definitely an argument that
|
||||
subscriptions should only ever source from `app-db` BUT that it is
|
||||
event handlers which start and stop the sourcing of data from
|
||||
remote places.
|
||||
|
||||
Sorry, but you'll have to work out which of these two variations
|
||||
works best for you.
|
||||
|
||||
Within this document the first alternative has been given more word count
|
||||
only because there's a few more tricks to make it work, not because it
|
||||
is necessarily preferred.
|
||||
|
||||
## What Not To Do
|
||||
|
||||
Don't get into making views source their data directly using React liefcycle methods.
|
||||
|
||||
Sometimes, because of their background with other JS frameworks,
|
||||
new re-framers feel like the Components themselves (the views)
|
||||
should have the responsibility of sourcing the data they need.
|
||||
|
||||
They then use React lifecycle methods like `:component-did-mount`
|
||||
to load remote data.
|
||||
|
||||
I believe this is absolutely the wrong way to do it.
|
||||
|
||||
In re-frame we want views to be as simple and dumb as possible. They turn
|
||||
data into HTML and nothing more. they absolutely do not do imperative stuff.
|
||||
|
||||
Use one of the two alternatives described above.
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
## Talking To Servers
|
||||
|
||||
This page describes how a re-frame app might "talk" to a backend HTTP server.
|
||||
|
||||
We'll assume there's a json-returning server endpoint
|
||||
at "http://json.my-endpoint.com/blah". We want to GET from that
|
||||
endpoint and put a processed version of the returned json into `app-db`.
|
||||
|
||||
## Triggering The Request
|
||||
|
||||
The user often does something to trigger the process.
|
||||
|
||||
Here's a button which the user could click:
|
||||
```clj
|
||||
(defn request-it-button
|
||||
[]
|
||||
[:div {:class "button-class"
|
||||
:on-click #(dispatch [:request-it])} ;; get data from the server !!
|
||||
"I want it, now!"])
|
||||
```
|
||||
|
||||
Notice the `on-click` handler - it `dispatch`es the event `[:request-it]`.
|
||||
|
||||
## The Event Handler
|
||||
|
||||
That `:request-it` event will need to be "handled", which means an event handler must be registered for it.
|
||||
|
||||
We want this handler to:
|
||||
1. Initiate the HTTP GET
|
||||
2. Update a flag in `app-db` which will trigger a modal "Loading ..." message for the user to see
|
||||
|
||||
We're going to create two versions of this event handler. First, we'll create a
|
||||
problematic version of the event handler and then, realising our sins, we'll write
|
||||
a second version which is a soaring paragon of virtue. Both versions
|
||||
will teach us something.
|
||||
|
||||
|
||||
### Version 1
|
||||
|
||||
We're going to use the [cljs-ajax library](https://github.com/JulianBirch/cljs-ajax) as the HTTP workhorse.
|
||||
|
||||
Here's the event handler:
|
||||
```clj
|
||||
(ns my.app.events ;; <1>
|
||||
(:require [ajax.core :refer [GET]]
|
||||
[re-frame.core :refer [re-event-db]))
|
||||
|
||||
(reg-event-db ;; <-- register an event handler
|
||||
:request-it ;; <-- the event id
|
||||
(fn ;; <-- the handler function
|
||||
[db _]
|
||||
|
||||
;; kick off the GET, making sure to supply a callback for success and failure
|
||||
(GET
|
||||
"http://json.my-endpoint.com/blah"
|
||||
{:handler #(dispatch [:process-response %1]) ;; <2> further dispatch !!
|
||||
:error-handler #(dispatch [:bad-response %1])}) ;; <2> further dispatch !!
|
||||
|
||||
;; update a flag in `app-db` ... presumably to cause a "Loading..." UI
|
||||
(assoc db :loading? true))) ;; <3> return an updated db
|
||||
```
|
||||
|
||||
Further Notes:
|
||||
1. Event handlers are normally put into an `events.cljs` namespace
|
||||
2. Notice that the GET callbacks issue a further `dispatch`. Such callbacks
|
||||
should never attempt to close over `db` themselves, or make
|
||||
any changes to it because, by the time these callbacks happen, the value
|
||||
in `app-db` may have changed. Whereas, if they `dispatch`, then the event
|
||||
handlers looking after the event they dispatch will be given the latest copy of the db.
|
||||
3. event handlers registered using `reg-event-db` must return a new value for
|
||||
`app-db`. In our case, we set a flag which will presumably cause a "Loading ..."
|
||||
UI to show.
|
||||
|
||||
### Successful GET
|
||||
|
||||
As we noted above, the on-success handler itself is just
|
||||
`(dispatch [:process-response RESPONSE])`. So we'll need to register a handler
|
||||
for this event too.
|
||||
|
||||
Like this:
|
||||
```clj
|
||||
(reg-event-db
|
||||
:process-response
|
||||
(fn
|
||||
[db [_ response]] ;; destructure the response from the event vector
|
||||
(-> db
|
||||
(assoc :loading? false) ;; take away that "Loading ..." UI
|
||||
(assoc :data (js->clj response)))) ;; fairly lame processing
|
||||
```
|
||||
|
||||
A normal handler would have more complex processing of the response. But we're
|
||||
just sketching here, so we've left it easy.
|
||||
|
||||
There'd also need to be a handler for the `:bad-response` event too. Left as an exercise.
|
||||
|
||||
### Problems In Paradise?
|
||||
|
||||
This approach will work, and it is useful to take time to understand why it
|
||||
would work, but it has a problem: the event handler isn't pure.
|
||||
|
||||
That `GET` is a side effect, and side effecting functions are like a
|
||||
well salted paper cut. We try hard to avoid them.
|
||||
|
||||
### Version 2
|
||||
|
||||
The better solution is, of course, to use an effectful handler. This
|
||||
is explained in detail in the previous tutorials: [Effectful Handlers](EffectfulHandler.md)
|
||||
and [Effects](Effects.md).
|
||||
|
||||
In the 2nd version, we use the alternative registration function, `reg-event-fx` , and we'll use an
|
||||
"Effect Handler" supplied by this library
|
||||
[https://github.com/Day8/re-frame-http-fx](https://github.com/Day8/re-frame-http-fx).
|
||||
You may soon feel confident enough to write your own.
|
||||
|
||||
Here's our rewrite:
|
||||
```clj
|
||||
(ns my.app.events
|
||||
(:require
|
||||
[day8.re-frame.http-fx]
|
||||
[re-frame.core :refer [re-event-fx]))
|
||||
|
||||
(reg-event-fx ;; <-- note the `-fx` extension
|
||||
:request-it ;; <-- the event id
|
||||
(fn ;; <-- the handler function
|
||||
[{db :db} _] ;; <-- 1st argument is coeffect, from which we extract db
|
||||
|
||||
;; we return a map of (side) effects
|
||||
{:http-xhrio {:method :get
|
||||
:uri "http://json.my-endpoint.com/blah"
|
||||
:on-success [:process-response]
|
||||
:on-failure [:bad-response]}
|
||||
:db (assoc db :loading? true)}))
|
||||
```
|
||||
|
||||
Notes:
|
||||
1. Our event handler "describes" side effects, it does not "do" side effects
|
||||
2. The event handler we wrote for `:process-response` stays as it was
|
||||
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ handler, making this common case easy to program.
|
|||
|
||||
But sometimes an event handler needs other data inputs
|
||||
to perform its computation. Things like a random number, or a GUID,
|
||||
or the current datetime. It might even need access to a
|
||||
or the current datetime. Perhaps it needs access to a
|
||||
DataScript connection.
|
||||
|
||||
|
||||
|
@ -50,17 +50,17 @@ This handler obtains data directly from LocalStore:
|
|||
(assoc db :defaults val))))
|
||||
```
|
||||
|
||||
This works, but there's a cost.
|
||||
This works, but there's a cost.
|
||||
|
||||
Because it has directly accessed LocalStore, this event handler is not
|
||||
pure, and impure functions cause well-documented paper cuts.
|
||||
pure, and impure functions cause well-documented paper cuts.
|
||||
|
||||
### How We Want It
|
||||
|
||||
Our goal in this tutorial is to rewrite this event handler so
|
||||
that it __only__ uses data from arguments.
|
||||
Our goal in this tutorial will be to rewrite this event handler so
|
||||
that it __only__ uses data from arguments. This will take a few steps.
|
||||
|
||||
To make this happen, we first switch to
|
||||
The first is that we first switch to
|
||||
using `reg-event-fx` (instead of `reg-event-db`).
|
||||
|
||||
Event handlers registered via `reg-event-fx` are slightly
|
||||
|
@ -85,8 +85,9 @@ right value. Nice! But how do we make this magic happen?
|
|||
|
||||
### Abracadabra
|
||||
|
||||
Each time an event handler is executed, a brand new `context` is created, and within that
|
||||
`context` is a brand new `:coeffect` map, which is initially totally empty.
|
||||
Each time an event handler is executed, a brand new `context`
|
||||
is created, and within that `context` is a brand new `:coeffect`
|
||||
map, which is initially totally empty.
|
||||
|
||||
That pristine `context` value (containing a pristine `:coeffect` map) is threaded
|
||||
through a chain of Interceptors before it finally reaches our event handler,
|
||||
|
@ -114,11 +115,11 @@ Something like this (this handler is the same as before, except for one detail):
|
|||
{:db (assoc db :defaults val))}))
|
||||
```
|
||||
|
||||
Look at that - my event handler has a new Interceptor! It is injecting the right key/value pair (`:local-store`)
|
||||
into `context's` `:coeffeects`, which then goes on to be the first argument
|
||||
Look at that - my event handler has a new Interceptor! It is injecting the
|
||||
right key/value pair (`:local-store`)
|
||||
into `context's` `:coeffeects`, which itself then goes on to be the first argument
|
||||
to our event handler (`cofx`).
|
||||
|
||||
|
||||
### `inject-cofx`
|
||||
|
||||
`inject-cofx` is part of the re-frame API.
|
||||
|
@ -243,14 +244,13 @@ In your test, you'd mock out the cofx handler:
|
|||
If your test does alter registered coeffect handlers, and you are using `cljs.test`,
|
||||
then you can use a `fixture` to restore all coeffects at the end of your test:
|
||||
```clj
|
||||
(defn re-frame-fixture
|
||||
[f]
|
||||
(let [restore-re-frame-fn (re-frame.core/make-restore-fn)]
|
||||
(try
|
||||
(f)
|
||||
(finally (restore-re-frame-fn)))))
|
||||
|
||||
(cljs.test/use-fixtures :each re-frame-fixture)
|
||||
(defn fixture-re-frame
|
||||
[]
|
||||
(let [restore-re-frame (atom nil)]
|
||||
{:before #(reset! restore-re-frame (re-frame.core/make-restore-fn))
|
||||
:after #(@restore-re-frame)}))
|
||||
|
||||
(use-fixtures :each (fixture-re-frame))
|
||||
```
|
||||
|
||||
`re-frame.core/make-restore-fn` creates a checkpoint for re-frame state (including
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 188 KiB |
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
|
@ -1,5 +1,38 @@
|
|||
**re-frame logo**
|
||||
## The re-frame Logo
|
||||
|
||||
![logo](/images/logo/re-frame_256w.png?raw=true)
|
||||
|
||||
Created by the mysterious @martinklepsch
|
||||
|
||||
Some say he appears on high value stamps in Germany and that he once
|
||||
punched a horse to the ground. Others say he loves recursion so much
|
||||
that in his wallet he keeps a photograph of his wallet.
|
||||
|
||||
All we know for sure is that he wields [Sketch.app](https://www.sketchapp.com/) like
|
||||
Bruce Lee wielded nunchucks.
|
||||
|
||||
## Genesis Theories
|
||||
|
||||
Great, unexplained works always encourage fan theories, and the re-frame logo
|
||||
is no exception.
|
||||
|
||||
Some speculate @martinklepsch created it as a bifarious rainbow omage
|
||||
to a utilities room in Frank Lloyd Wright's Guggenheim.
|
||||
|
||||
![](Guggenheim.jpg)
|
||||
|
||||
<br><br>
|
||||
Others see the cljs logo folded across re-frame's official
|
||||
architecture diagram, forming a flowing poststructuralist rebuttal of OO's
|
||||
duplicate letter adjacency, and Jackson Pollock's Fractal Expressionism.
|
||||
|
||||
|
||||
![](Genesis.png)
|
||||
|
||||
You be the judge.
|
||||
|
||||
### Instructions
|
||||
|
||||
Use [Sketch.app](https://www.sketchapp.com/) to update the `re-frame-logo.sketch` file.
|
||||
|
||||
Unfortunately the gradients are not exported properly so we can't provide an SVG here for now.
|
||||
Unfortunately the gradients are not exported properly so we can't provide an SVG here for now.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
(defproject re-frame "0.8.0"
|
||||
(defproject re-frame "0.8.1"
|
||||
:description "A Clojurescript MVC-like Framework For Writing SPAs Using Reagent."
|
||||
:url "https://github.com/Day8/re-frame.git"
|
||||
:license {:name "MIT"}
|
||||
|
@ -9,7 +9,7 @@
|
|||
|
||||
:profiles {:debug {:debug true}
|
||||
:dev {:dependencies [[karma-reporter "0.3.0"]
|
||||
[binaryage/devtools "0.7.2"]]
|
||||
[binaryage/devtools "0.8.1"]]
|
||||
:plugins [[lein-cljsbuild "1.1.3"]
|
||||
[lein-npm "0.6.2"]
|
||||
[lein-figwheel "0.5.4-7"]
|
||||
|
@ -45,7 +45,9 @@
|
|||
|
||||
:cljsbuild {:builds [{:id "test"
|
||||
:source-paths ["test" "src"]
|
||||
:compiler {:output-to "run/compiled/browser/test.js"
|
||||
:compiler {:preloads [devtools.preload]
|
||||
:external-config {:devtools/config {:features-to-install :all}}
|
||||
:output-to "run/compiled/browser/test.js"
|
||||
:source-map true
|
||||
:output-dir "run/compiled/browser/test"
|
||||
:optimizations :none
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
(ns re-frame.core
|
||||
(:require
|
||||
[re-frame.events :as events]
|
||||
[re-frame.subs :as subs]
|
||||
[re-frame.interop :as interop]
|
||||
[re-frame.db :as db]
|
||||
[re-frame.fx :as fx]
|
||||
[re-frame.cofx :as cofx]
|
||||
[re-frame.router :as router]
|
||||
[re-frame.loggers :as loggers]
|
||||
[re-frame.registrar :as registrar]
|
||||
[re-frame.interceptor :as interceptor]
|
||||
[re-frame.events :as events]
|
||||
[re-frame.subs :as subs]
|
||||
[re-frame.interop :as interop]
|
||||
[re-frame.db :as db]
|
||||
[re-frame.fx :as fx]
|
||||
[re-frame.cofx :as cofx]
|
||||
[re-frame.router :as router]
|
||||
[re-frame.loggers :as loggers]
|
||||
[re-frame.registrar :as registrar]
|
||||
[re-frame.interceptor :as interceptor]
|
||||
[re-frame.std-interceptors :as std-interceptors :refer [db-handler->interceptor
|
||||
fx-handler->interceptor
|
||||
ctx-handler->interceptor]]))
|
||||
|
@ -67,7 +67,7 @@
|
|||
"Register the given `id`, typically a keyword, with the combination of
|
||||
`db-handler` and an interceptor chain.
|
||||
`db-handler` is a function: (db event) -> db
|
||||
`interceptors` is a collection of interceptors, possibly nested (needs flattenting).
|
||||
`interceptors` is a collection of interceptors, possibly nested (needs flattening).
|
||||
`db-handler` is wrapped in an interceptor and added to the end of the chain, so in the end
|
||||
there is only a chain.
|
||||
The necessary effects and coeffects handler are added to the front of the
|
||||
|
@ -102,7 +102,7 @@
|
|||
(def set-loggers! loggers/set-loggers!)
|
||||
|
||||
;; If you are writing an extension to re-frame, like perhaps
|
||||
;; an effeects handler, you may want to use re-frame logging.
|
||||
;; an effects handler, you may want to use re-frame logging.
|
||||
;;
|
||||
;; usage: (console :error "this is bad: " a-variable " and " anotherv)
|
||||
;; (console :warn "possible breach of containment wall at: " dt)
|
||||
|
@ -137,10 +137,10 @@
|
|||
nil)))
|
||||
|
||||
|
||||
;; -- Event Procssing Callbacks
|
||||
;; -- Event Processing Callbacks
|
||||
|
||||
(defn add-post-event-callback
|
||||
"Registers a function `f` to be called after each event is procecessed
|
||||
"Registers a function `f` to be called after each event is processed
|
||||
`f` will be called with two arguments:
|
||||
- `event`: a vector. The event just processed.
|
||||
- `queue`: a PersistentQueue, possibly empty, of events yet to be processed.
|
||||
|
@ -166,7 +166,7 @@
|
|||
|
||||
|
||||
;; -- Deprecation Messages
|
||||
;; Assisting the v0.0.7 -> v0.0.8 tranistion.
|
||||
;; Assisting the v0.0.7 -> v0.0.8 transition.
|
||||
(defn register-handler
|
||||
[& args]
|
||||
(console :warn "re-frame: \"register-handler\" has been renamed \"reg-event-db\" (look for registration of " (str (first args)) ")")
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
;; -- Application State --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Should not be accessed directly by application code
|
||||
;; Should not be accessed directly by application code.
|
||||
;; Read access goes through subscriptions.
|
||||
;; Updates via event handlers.
|
||||
(def app-db (ratom {}))
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"Associate the given event `id` with the given collection of `interceptors`.
|
||||
|
||||
`interceptors` may contain nested collections and there may be nils
|
||||
at any level,so process this sturcuture into a simple, nil-less vector
|
||||
at any level,so process this structure into a simple, nil-less vector
|
||||
before registration.
|
||||
|
||||
An `event handler` will likely be at the end of the chain (wrapped in an interceptor)."
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
(ns re-frame.fx
|
||||
(:require
|
||||
[re-frame.router :as router]
|
||||
[re-frame.db :refer [app-db]]
|
||||
[re-frame.router :as router]
|
||||
[re-frame.db :refer [app-db]]
|
||||
[re-frame.interceptor :refer [->interceptor]]
|
||||
[re-frame.interop :refer [set-timeout!]]
|
||||
[re-frame.events :as events]
|
||||
[re-frame.registrar :refer [get-handler clear-handlers register-handler]]
|
||||
[re-frame.loggers :refer [console]]))
|
||||
[re-frame.interop :refer [set-timeout!]]
|
||||
[re-frame.events :as events]
|
||||
[re-frame.registrar :refer [get-handler clear-handlers register-handler]]
|
||||
[re-frame.loggers :refer [console]]))
|
||||
|
||||
|
||||
;; -- Registration ------------------------------------------------------------
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
|
||||
(defn ->interceptor
|
||||
"Create an interceptor from named arguements"
|
||||
"Create an interceptor from named arguments"
|
||||
[& {:as m :keys [name id before after]}] ;; XXX remove `name` in due course - only in there as a backwards compat thing
|
||||
(when debug-enabled?
|
||||
(if name ;; XXX remove in due course
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
(def after-render reagent.core/after-render)
|
||||
|
||||
;; Make sure the Google Closure compiler sees this as a boolean constatnt,
|
||||
;; Make sure the Google Closure compiler sees this as a boolean constant,
|
||||
;; otherwise Dead Code Elimination won't happen in `:advanced` builds.
|
||||
;; Type hints have been liberally sprinkled.
|
||||
;; https://developers.google.com/closure/compiler/docs/js-for-compiler
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
with a `handler` (function). This namespace contains the
|
||||
central registry of such associations."
|
||||
(:require [re-frame.interop :refer [debug-enabled?]]
|
||||
[re-frame.loggers :refer [console]]))
|
||||
[re-frame.loggers :refer [console]]))
|
||||
|
||||
|
||||
;; kinds of handlers
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
(ns re-frame.router
|
||||
(:require [re-frame.events :refer [handle]]
|
||||
(:require [re-frame.events :refer [handle]]
|
||||
[re-frame.interop :refer [after-render empty-queue next-tick]]
|
||||
[re-frame.loggers :refer [console]]))
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
|||
;;
|
||||
;; A call to "re-frame.core/dispatch" places an event on a queue for processing.
|
||||
;; A short time later, the handler registered to handle this event will be run.
|
||||
;; What follows is the implemtation of this process.
|
||||
;; What follows is the implementation of this process.
|
||||
;;
|
||||
;; The task is to process queued events in a perpetual loop, one after
|
||||
;; the other, FIFO, calling the registered event-handler for each, being idle when
|
||||
|
@ -32,14 +32,14 @@
|
|||
;; - maintain a FIFO queue of `dispatched` events.
|
||||
;; - when a new event arrives, "schedule" processing of this queue using
|
||||
;; goog.async.nextTick, which means it will happen "very soon".
|
||||
;; - when processing events, one after the other, do ALL the those currently
|
||||
;; queued. Don't stop. Don't yield to the browser. Hog that CPU.
|
||||
;; - when processing events, one after the other, do ALL the currently
|
||||
;; queued events. Don't stop. Don't yield to the browser. Hog that CPU.
|
||||
;; - but if any new events are dispatched during this cycle of processing,
|
||||
;; don't do them immediately. Leave them queued. Yield first to the browser,
|
||||
;; and do these new events in the next processing cycle. That way we drain
|
||||
;; the queue up to a point, but we never hog the CPU forever. In
|
||||
;; particular, we handle the case where handling one event will beget
|
||||
;; another event. The freshly begatted event will be handled next cycle,
|
||||
;; another event. The freshly begotten event will be handled next cycle,
|
||||
;; with yielding in-between.
|
||||
;; - In some cases, an event should not be handled until after the GUI has been
|
||||
;; updated, i.e., after the next Reagent animation frame. In such a case,
|
||||
|
@ -118,9 +118,9 @@
|
|||
(-fsm-trigger
|
||||
[this trigger arg]
|
||||
|
||||
;; The following "case" impliments the Finite State Machine.
|
||||
;; The following "case" implements the Finite State Machine.
|
||||
;; Given a "trigger", and the existing FSM state, it computes the
|
||||
;; new FSM state and the tranistion action (function).
|
||||
;; new FSM state and the transition action (function).
|
||||
|
||||
(let [[new-fsm-state action-fn]
|
||||
(case [fsm-state trigger]
|
||||
|
@ -237,7 +237,7 @@
|
|||
|
||||
|
||||
(defn dispatch-sync
|
||||
"Sychronously (immediaetly!) process the given event using the registered handler.
|
||||
"Sychronously (immediately!) process the given event using the registered handler.
|
||||
|
||||
Generally, you shouldn't use this - you should use `dispatch` instead. It
|
||||
is an error to use `dispatch-sync` within an event handler.
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
(ns re-frame.subs
|
||||
(:require
|
||||
[re-frame.db :refer [app-db]]
|
||||
[re-frame.interop :refer [add-on-dispose! debug-enabled? make-reaction ratom? deref?]]
|
||||
[re-frame.loggers :refer [console]]
|
||||
[re-frame.utils :refer [first-in-vector]]
|
||||
[re-frame.db :refer [app-db]]
|
||||
[re-frame.interop :refer [add-on-dispose! debug-enabled? make-reaction ratom? deref?]]
|
||||
[re-frame.loggers :refer [console]]
|
||||
[re-frame.utils :refer [first-in-vector]]
|
||||
[re-frame.registrar :refer [get-handler clear-handlers register-handler]]))
|
||||
|
||||
|
||||
|
@ -34,7 +34,7 @@
|
|||
"cache the reaction r"
|
||||
[query-v dynv r]
|
||||
(let [cache-key [query-v dynv]]
|
||||
;; when this reaction is nolonger being used, remove it from the cache
|
||||
;; when this reaction is no longer being used, remove it from the cache
|
||||
(add-on-dispose! r #(do (swap! query->reaction dissoc cache-key)
|
||||
#_(console :log "Removing subscription:" cache-key)))
|
||||
;; cache this reaction, so it can be used to deduplicate other, later "=" subscriptions
|
||||
|
@ -118,7 +118,7 @@
|
|||
(subs/subscribe [:b-sub])])
|
||||
(fn [[a b] [_]] {:a a :b b}))
|
||||
|
||||
Two functions provided. The 2nd is computation fucntion, as before. The 1st
|
||||
Two functions provided. The 2nd is computation function, as before. The 1st
|
||||
is returns what `input signals` should be provided to the computation. The
|
||||
`input signals` function is called with two arguments: the query vector
|
||||
and the dynamic vector. The return value can be singleton reaction or
|
||||
|
@ -135,7 +135,7 @@
|
|||
"
|
||||
[query-id & args]
|
||||
(let [computation-fn (last args)
|
||||
input-args (butlast args) ;; may be empty, or one fn, or pairs of :<- / vetor
|
||||
input-args (butlast args) ;; may be empty, or one fn, or pairs of :<- / vector
|
||||
err-header (str "re-frame: reg-sub for " query-id ", ")
|
||||
inputs-fn (case (count input-args)
|
||||
;; no `inputs` function provided - give the default
|
||||
|
|
|
@ -7,12 +7,15 @@
|
|||
|
||||
;; ---- FIXTURES ---------------------------------------------------------------
|
||||
|
||||
(defn teardown! []
|
||||
; cleanup up our handlers
|
||||
(doseq [event [::later-test ::watcher]]
|
||||
(re-frame/clear-event event)))
|
||||
;; This fixture uses the re-frame.core/make-restore-fn to checkpoint and reset
|
||||
;; to cleanup any dynamically registered handlers from our tests.
|
||||
(defn fixture-re-frame
|
||||
[]
|
||||
(let [restore-re-frame (atom nil)]
|
||||
{:before #(reset! restore-re-frame (re-frame.core/make-restore-fn))
|
||||
:after #(@restore-re-frame)}))
|
||||
|
||||
(use-fixtures :each {:after teardown!})
|
||||
(use-fixtures :each (fixture-re-frame))
|
||||
|
||||
;; ---- TESTS ------------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -3,14 +3,12 @@
|
|||
(:require
|
||||
[cljs.test :as cljs-test :include-macros true]
|
||||
[jx.reporter.karma :as karma :include-macros true]
|
||||
[devtools.core :as devtools]
|
||||
;; Test Namespaces -------------------------------
|
||||
[re-frame.interceptor-test]
|
||||
[re-frame.subs-test]
|
||||
[re-frame.fx-test]))
|
||||
|
||||
(enable-console-print!)
|
||||
(devtools/install! [:custom-formatters :sanity-hints]) ;; we love https://github.com/binaryage/cljs-devtools
|
||||
|
||||
;; ---- BROWSER based tests ----------------------------------------------------
|
||||
(defn ^:export set-print-fn! [f]
|
||||
|
|
Loading…
Reference in New Issue