Merge branch 'develop' into re-frame-tweaks

# Conflicts:
#	src/re_frame/std_interceptors.cljc
This commit is contained in:
Mike Thompson 2016-08-27 12:48:00 +10:00
commit a5d3c6020f
27 changed files with 896 additions and 96 deletions

View File

@ -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

View File

@ -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.

View File

@ -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)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Interceptors](Interceptors.md)

View File

@ -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)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](Readme.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Coeffects](Coeffects.md)
---
### Builtin Effect Handlers

View File

@ -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)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](Readme.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Effects](Effects.md)
## Appendix

View File

@ -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)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](Readme.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Talking To Servers](Talking-To-Servers.md)

View File

@ -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.

49
docs/Navigation.md Normal file
View File

@ -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.

View File

@ -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)

View File

@ -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.

140
docs/Talking-To-Servers.md Normal file
View File

@ -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

View File

@ -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

BIN
images/logo/Genesis.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
images/logo/Guggenheim.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -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.

View File

@ -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

View File

@ -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)) ")")

View File

@ -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 {}))

View File

@ -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)."

View File

@ -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 ------------------------------------------------------------

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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 ------------------------------------------------------------------

View File

@ -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]