Merge branch 'develop'
|
@ -0,0 +1 @@
|
|||
CHANGES.md merge=union
|
|
@ -4,7 +4,11 @@
|
|||
<option name="PER_PROJECT_SETTINGS">
|
||||
<value>
|
||||
<ClojureCodeStyleSettings>{
|
||||
:cljs.core/with-redefs 1
|
||||
:cursive.formatting/align-binding-forms true
|
||||
:cursive.formatting/comment-align-column 0
|
||||
:re-frame.trace/register-trace-cb :only-indent
|
||||
:re-frame.trace/with-trace 1
|
||||
}</ClojureCodeStyleSettings>
|
||||
<XML>
|
||||
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
|
||||
|
|
39
CHANGES.md
|
@ -1,9 +1,32 @@
|
|||
## 0.8.1 (2016.08.XX) Unreleased
|
||||
## 0.9.0 (2016.12.15)
|
||||
|
||||
Welcome Guests. Dr Ford has created a new [6-part narrative](README.md),
|
||||
and Bernard [some infographics](/docs/EventHandlingInfographic.md). Anyone seen Dolores?
|
||||
|
||||
#### Headline
|
||||
|
||||
- The [README](README.md) and [/docs](/docs/README.md) have been substantially reworked.
|
||||
- [#218](https://github.com/Day8/re-frame/issues/218) Make it okay to use `subscribe` in Form-1 components. This is a big deal.
|
||||
|
||||
#### Breaking
|
||||
|
||||
- Due to the new tracing features using `goog-define` (described below), re-frame now requires ClojureScript 1.7.48 or above. See [Parameterizing ClojureScript Builds](https://www.martinklepsch.org/posts/parameterizing-clojurescript-builds.html) for more information.
|
||||
|
||||
#### Improvements
|
||||
|
||||
- [#200](https://github.com/Day8/re-frame/pull/200) Remove trailing spaces from console logging
|
||||
- [#200](https://github.com/Day8/re-frame/pull/200) Remove trailing spaces from console logging
|
||||
- Add `re-frame.loggers/get-loggers` function to well, you know.
|
||||
- Added `clear-subscription-cache!` function. This should be used when hot reloading code to ensure that any bad subscriptions that cause rendering exceptions are removed. See [reagent-project/reagent#272](https://github.com/reagent-project/reagent/issues/272) for more details.
|
||||
- Added experimental tracing features. These are subject to change and remain undocumented at the moment. By default they are disabled, and will be completely compiled out by advanced optimisations. To enable them, set a [`:closure-defines`](https://www.martinklepsch.org/posts/parameterizing-clojurescript-builds.html) key to `{"re_frame.trace.trace_enabled_QMARK_" true}`
|
||||
- [#223](https://github.com/Day8/re-frame/issues/223) When using `make-restore-fn`, dispose of any subscriptions that were created after the restore function was created.
|
||||
- [#283](https://github.com/Day8/re-frame/pull/283) Make trim-v interceptor symmetrical, so it adds the missing event id back on to the `:event` coeffect in the `:after` function.
|
||||
|
||||
#### Fixes
|
||||
|
||||
- [#259](https://github.com/Day8/re-frame/pull/259) Fix a bug where registering a subscription would create and close over dependent subscriptions, meaning that they would never be garbage collected, and doing more work than necessary.
|
||||
- Fix a bug where subscribing to a subscription that didn't exist would throw an exception, instead of returning nil.
|
||||
- [#248](https://github.com/Day8/re-frame/pull/248) Provide after interceptor with `db` coeffect, if no `db` effect was produced.
|
||||
- [#278](https://github.com/Day8/re-frame/issues/278) Provide enrich interceptor with `db` coeffect, if no `db` effect was produced.
|
||||
|
||||
## 0.8.0 (2016.08.19)
|
||||
|
||||
|
@ -67,7 +90,7 @@ Joking aside, this is a substantial release which will change how you use re-fra
|
|||
Reagent available). But you can debug your event handler tests using full JVM tooling goodness.
|
||||
|
||||
@samroberton and @escherize have provided the thought leadership and drive here. They converted
|
||||
re-frame to `.cljc`, supplying plugable interop for both the `js` and `jvm` platforms.
|
||||
re-frame to `.cljc`, supplying pluggable interop for both the `js` and `jvm` platforms.
|
||||
|
||||
Further, they have worked with @danielcompton to create a library of testing utilities which
|
||||
will hopefully evolve into a nice step forward on both platforms: <br>
|
||||
|
@ -93,7 +116,7 @@ Joking aside, this is a substantial release which will change how you use re-fra
|
|||
successful part of the framework. We thought we were happy.
|
||||
|
||||
But recently @steveb8n gave a cljsyd talk on
|
||||
Pedistal's Interceptor pattern which suddenly transformed them from
|
||||
Pedestal's Interceptor pattern which suddenly transformed them from
|
||||
arcane to delightfully simple in 20 mins. Interceptors are
|
||||
really "middleware via data" rather than "middleware via higher order functions".
|
||||
So it is another way of doing the same thing, but thanks to @steveb8n
|
||||
|
@ -176,7 +199,7 @@ Joking aside, this is a substantial release which will change how you use re-fra
|
|||
|
||||
Breaking:
|
||||
- removed middleware `log-ex`. It is no longer needed because browsers now correctly report the
|
||||
throw site of re-thown exceptions. In the unlikely event that you absolutely still need it,
|
||||
throw site of re-thrown exceptions. In the unlikely event that you absolutely still need it,
|
||||
the source for `log-ex` is still in `middleware.cljs`, commented out. Just transfer it to your project.
|
||||
|
||||
- `debug` middleware now produces slightly different output (to console). So no code will need to change,
|
||||
|
@ -197,8 +220,8 @@ Fixed:
|
|||
|
||||
New API:
|
||||
- [#118](https://github.com/Day8/re-frame/pull/118) - Add `add-post-event-callback` to the API.
|
||||
@pupeno is developing [preprender](https://carouselapps.com/prerenderer) which looks pretty neat.
|
||||
Support this effort by adding a way for preprender to hook event processing.
|
||||
@pupeno is developing [prerenderer](https://carouselapps.com/prerenderer) which looks pretty neat.
|
||||
Support this effort by adding a way for prerenderer to hook event processing.
|
||||
|
||||
- `on-changes` middleware now official. No longer experimental.
|
||||
|
||||
|
@ -250,7 +273,7 @@ Headline:
|
|||
- mean apps, in production, stand a chance of reporting UHE
|
||||
to the user, and can perhaps even recover to a sane state.
|
||||
- #53 Fix Logging And Error Reporting
|
||||
You can now provide your own logging fucntions.
|
||||
You can now provide your own logging functions.
|
||||
Further explanation [here](https://github.com/Day8/re-frame/wiki/FAQ#3-can-re-frame-use-my-logging-functions).
|
||||
|
||||
Deprecated:
|
||||
|
|
|
@ -12,7 +12,6 @@ the [ClojureScript mailing list](https://groups.google.com/forum/#!forum/clojure
|
|||
|
||||
**Create pull requests to the develop branch**, work will be merged onto master when it is ready to be released.
|
||||
|
||||
|
||||
## Running tests
|
||||
|
||||
#### Via Browser/HTML
|
||||
|
@ -60,3 +59,9 @@ If possible provide:
|
|||
* Docstrings for functions
|
||||
* Documentation examples
|
||||
* Add the change to the Unreleased section of [CHANGES.md](CHANGES.md)
|
||||
|
||||
## Pull requests for docs
|
||||
|
||||
* Make your documentation changes
|
||||
* (Optional) Install doctoc with `npm install -g doctoc`
|
||||
* (Optional) Regenerate the docs TOC with `bin/doctoc.sh` or `bin/doctoc.bat` depending on your OS
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
:: Table of contents are generated by doctoc.
|
||||
:: Install doctoc with `npm install -g doctoc`
|
||||
:: Then run this script to regenerate the TOC after
|
||||
:: editing the docs.
|
||||
|
||||
doctoc ./docs/ --github --title '## Table Of Contents'
|
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
# Table of contents are generated by doctoc.
|
||||
# Install doctoc with `npm install -g doctoc`
|
||||
# Then run this script to regenerate the TOC after
|
||||
# editing the docs.
|
||||
|
||||
doctoc $(dirname $0)/../docs --github --title '## Table Of Contents'
|
|
@ -0,0 +1,128 @@
|
|||
- [On Data](#on-data)
|
||||
- [The Big Ratom](#the-big-ratom)
|
||||
- [The Benefits Of Data-In-The-One-Place](#the-benefits-of-data-in-the-one-place)
|
||||
|
||||
|
||||
#### Table Of Contents
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
- [On Data](#on-data)
|
||||
- [The Big Ratom](#the-big-ratom)
|
||||
- [The Benefits Of Data-In-The-One-Place](#the-benefits-of-data-in-the-one-place)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
### On Data
|
||||
|
||||
<blockquote class="twitter-tweet" lang="en"><p>Well-formed Data at rest is as close to perfection in programming as it gets. All the crap that had to happen to put it there however...</p>— Fogus (@fogus) <a href="https://twitter.com/fogus/status/454582953067438080">April 11, 2014</a></blockquote>
|
||||
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
|
||||
### The Big Ratom
|
||||
|
||||
re-frame puts all your application state into one place, which is
|
||||
called `app-db`.
|
||||
|
||||
Ideally, you will provide a spec for this data-in-the-one-place,
|
||||
[using a powerful and leverageable schema](http://clojure.org/about/spec).
|
||||
|
||||
Now, this advice is not the slightest bit controversial for 'real' databases, right?
|
||||
You'd happily put all your well-formed data into PostgreSQL.
|
||||
|
||||
But within a running application (in memory), there can be hesitation. If you have
|
||||
a background in OO, this data-in-one-place
|
||||
business is a really, really hard one to swallow. You've
|
||||
spent your life breaking systems into pieces, organised around behaviour and trying
|
||||
to hide state. I still wake up in a sweat some nights thinking about all
|
||||
that Clojure data lying around exposed and passive.
|
||||
|
||||
But, as Fogus reminds us, data at rest is quite perfect.
|
||||
|
||||
In re-frame, `app-db` is one of these:
|
||||
```clj
|
||||
(def app-db (reagent/atom {})) ;; a Reagent atom, containing a map
|
||||
```
|
||||
|
||||
Although it is a `Reagent atom` (hereafter `ratom`), I'd encourage
|
||||
you to think of it as an in-memory database. It will contain structured data.
|
||||
You will need to query that data. You will perform CRUD
|
||||
and other transformations on it. You'll often want to transact on this
|
||||
database atomically, etc. So "in-memory database"
|
||||
seems a more useful paradigm than plain old map-in-atom.
|
||||
|
||||
Further Notes:
|
||||
|
||||
1. `app-state` would probably be a more accurate name, but I choose `app-db` instead because
|
||||
I wanted to convey the database notion as strongly as possible.
|
||||
2. In the documentation and code, I make a distinction between `app-db` (the `ratom`) and
|
||||
`db` which is the (map) `value` currently stored **inside** this `ratom`. Be aware of that naming.
|
||||
3. re-frame creates and manages an `app-db` for you, so
|
||||
you don't need to declare one yourself (see the 1st FAQ if you want to inspect the value it holds).
|
||||
4. `app-db` doesn't actually have to be a `ratom` containing a map. It could, for example,
|
||||
be a [datascript](https://github.com/tonsky/datascript database). In fact, any database which
|
||||
can signal you when it changes would do. We'd love! to be using [datascript](https://github.com/tonsky/datascript database) - so damn cool -
|
||||
but we had too much data in our apps. If you were to use it, you'd have to tweak re-frame a bit and use [Posh](https://github.com/mpdairy/posh).
|
||||
|
||||
|
||||
### The Benefits Of Data-In-The-One-Place
|
||||
|
||||
1. Here's the big one: because there is a single source of truth, we write no
|
||||
code to synchronize state between many different stateful components. I
|
||||
cannot stress enough how significant this is. You end up writing less code
|
||||
and an entire class of bugs is eliminated.
|
||||
(This mindset is very different to OO which involves
|
||||
distributing state across objects, and then ensuring that state is synchronized, all the while
|
||||
trying to hide it, which is, when you think about it, quite crazy ... and I did it for years).
|
||||
|
||||
2. Because all app state is coalesced into one atom, it can be updated
|
||||
with a single `reset!`, which acts like a transactional commit. There is
|
||||
an instant in which the app goes from one state to the next, never a series
|
||||
of incremental steps which can leave the app in a temporarily inconsistent, intermediate state.
|
||||
Again, this simplicity causes a certain class of bugs or design problems to evaporate.
|
||||
|
||||
3. The data in `app-db` can be given a strong schema
|
||||
so that, at any moment, we can validate all the data in the application. **All of it!**
|
||||
We do this check after every single "event handler" runs (event handlers compute new state).
|
||||
And this enables us to catch errors early (and accurately). It increases confidence in the way
|
||||
that Types can increase confidence, only [a good schema can provide more
|
||||
**leverage** than types](https://www.youtube.com/watch?v=nqY4nUMfus8).
|
||||
|
||||
4. Undo/Redo [becomes straight forward to implement](https://github.com/Day8/re-frame-undo).
|
||||
It is easy to snapshot and restore one central value. Immutable data structures have a
|
||||
feature called `structural sharing` which means it doesn't cost much RAM to keep the last, say, 200
|
||||
snapshots. All very efficient.
|
||||
For certain categories of applications (eg: drawing applications) this feature is borderline magic.
|
||||
Instead of undo/redo being hard, disruptive and error prone, it becomes trivial.
|
||||
**But,** many web applications are not self contained
|
||||
data-wise and, instead, are dominated by data sourced from an authoritative remote database.
|
||||
For these applications, re-frame's `app-db` is mostly a local caching
|
||||
point, and being able to do undo/redo its state is meaningless because the authoritative
|
||||
source of data is elsewhere.
|
||||
|
||||
5. The ability to genuinely model control via FSMs (discussed later).
|
||||
|
||||
6. The ability to do time travel debugging, even in a production setting. More soon.
|
||||
|
||||
|
||||
### Get You A Leveragable Schema
|
||||
|
||||
You really need a schema for `app-db`.
|
||||
|
||||
Of course, that means you'll have to learn [spec](http://clojure.org/about/spec) and there's
|
||||
some overhead in that, so maybe, just maybe, in your initial experiments, you can
|
||||
get away without one. But not for long. Promise me you'll write a `spec`. Good.
|
||||
|
||||
The [todomvc example](https://github.com/Day8/re-frame/tree/master/examples/todomvc)
|
||||
shows how to use a spec. Look in `src/db.cljs` for the spec itself, and then in `src/events.cljs` for
|
||||
how to write code which checks `app-db` against this spec after every single event has been
|
||||
processed.
|
||||
|
||||
Specs are more leveragable than types. Watch how: <br>
|
||||
https://www.youtube.com/watch?v=VNTQ-M_uSo8
|
||||
|
||||
***
|
||||
|
||||
Previous: [This Repo's README](../README.md)
|
||||
Up: [Index](README.md)
|
||||
Next: [First Code Walk-Through](CodeWalkThrough.md)
|
|
@ -1,3 +1,13 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Simpler Apps](#simpler-apps)
|
||||
- [There's A Small Gotcha](#theres-a-small-gotcha)
|
||||
- [Larger Apps](#larger-apps)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Simpler Apps
|
||||
|
||||
To build a re-frame app, you:
|
||||
|
@ -58,7 +68,8 @@ src
|
|||
|
||||
Continue to [Navigation](Navigation.md) to learn how to switch between panels of a larger app.
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
Previous: [CoEffects](coeffects.md)
|
||||
Up: [Index](README.md)
|
||||
Next: [Navigation](Navigation.md)
|
||||
|
|
|
@ -0,0 +1,514 @@
|
|||
|
||||
|
||||
## Initial Code Walk-through
|
||||
|
||||
At this point, you are about 50% of the way to understanding re-frame. You are armed with:
|
||||
- a high level understanding of the 6 domino process (from re-frame's README)
|
||||
- an understanding of application state (from the previous tutorial)
|
||||
|
||||
By the end of this tutorial, you'll be at 70%, which is good
|
||||
enough to start coding by yourself.
|
||||
|
||||
In this tutorial, **we'll look at re-frame code**.
|
||||
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [What Code?](#what-code)
|
||||
- [What Does It Do?](#what-does-it-do)
|
||||
- [Namespace](#namespace)
|
||||
- [Data Schema](#data-schema)
|
||||
- [Events (domino 1)](#events-domino-1)
|
||||
- [dispatch](#dispatch)
|
||||
- [After dispatch](#after-dispatch)
|
||||
- [Event Handlers (domino 2)](#event-handlers-domino-2)
|
||||
- [reg-event-db](#reg-event-db)
|
||||
- [:initialize](#initialize)
|
||||
- [:timer](#timer)
|
||||
- [:time-color-change](#time-color-change)
|
||||
- [Effect Handlers (domino 3)](#effect-handlers-domino-3)
|
||||
- [Subscription Handlers (domino 4)](#subscription-handlers-domino-4)
|
||||
- [reg-sub](#reg-sub)
|
||||
- [View Functions (domino 5)](#view-functions-domino-5)
|
||||
- [Hiccup](#hiccup)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## What Code?
|
||||
|
||||
This repo contains an example application called ["simple"](https://github.com/Day8/re-frame/tree/develop/examples/simple),
|
||||
which has around 70 lines of code. We'll look at every line of [the file](https://github.com/Day8/re-frame/blob/develop/examples/simple/src/simple/core.cljs).
|
||||
|
||||
This app:
|
||||
- displays the current time in a nice big, colourful font
|
||||
- provides a single text input field, into which you can type a hex colour code,
|
||||
like "#CCC", used for the time display
|
||||
|
||||
Here's what it looks like:
|
||||
![Example App image](../images/example_app.png)
|
||||
|
||||
To run the code:
|
||||
* Install Java 8
|
||||
* Install leiningen (http://leiningen.org/#install)
|
||||
|
||||
Then:
|
||||
1. `git clone https://github.com/Day8/re-frame.git`
|
||||
2. `cd re-frame/examples/simple`
|
||||
3. `lein do clean, figwheel`
|
||||
4. open http://localhost:3449/example.html
|
||||
|
||||
|
||||
## Namespace
|
||||
|
||||
Because this example is tiny, the code is in a single namespace which you can find here:
|
||||
https://github.com/Day8/re-frame/blob/master/examples/simple/src/simpleexample/core.cljs
|
||||
|
||||
Within this namespace, we'll need access to both `reagent` and `re-frame`.
|
||||
So, at the top, we start like this:
|
||||
```clj
|
||||
(ns simple.core
|
||||
(:require [reagent.core :as reagent]
|
||||
[re-frame.core :as rf]))
|
||||
```
|
||||
|
||||
## Data Schema
|
||||
|
||||
Now, normally, I'd strongly recommended you write a quality schema
|
||||
for your application state (the data stored in `app-db`). But,
|
||||
here, to minimise cognitive load, we'll cut that corner.
|
||||
|
||||
But ... we can't cut it completely. You'll still need an
|
||||
informal description, and here it is ... for this app `app-db` will contain
|
||||
a two-key map like this:
|
||||
```cljs
|
||||
{:time (js/Date.) ;; current time for display
|
||||
:time-color "#f88"} ;; the colour in which the time should be be shown
|
||||
```
|
||||
|
||||
re-frame itself owns/manages `app-db` (see FAQ #1), and it will
|
||||
supply the value within it (a two-key map in this case)
|
||||
to your various handlers as required.
|
||||
|
||||
## Events (domino 1)
|
||||
|
||||
Events are data. You choose the format.
|
||||
|
||||
re-frame uses a vector
|
||||
format for events. For example:
|
||||
```clj
|
||||
[:delete-item 42]
|
||||
```
|
||||
|
||||
The first element in the vector identifies the `kind` of `event`. The
|
||||
further elements are optional, and can provide additional data
|
||||
associated with the event. The additional value above, `42`, is
|
||||
presumably the id of the item to delete.
|
||||
|
||||
Here are some other example events:
|
||||
|
||||
```clj
|
||||
[:admit-to-being-satoshi false]
|
||||
[:set-spam-wanted false :continue-harassment-nevertheless-flag]
|
||||
[:some-ns/on-GET-success response]
|
||||
```
|
||||
|
||||
The `kind` of event is always a keyword, and for non-trivial
|
||||
applications it tends to be namespaced.
|
||||
|
||||
**Rule**: events are pure data. No sneaky tricks like putting
|
||||
callback functions on the wire. You know who you are.
|
||||
|
||||
### dispatch
|
||||
|
||||
To send an event, call `dispatch` with the event vector as argument:
|
||||
```clj
|
||||
(dispatch [:event-id value1 value2])
|
||||
```
|
||||
|
||||
In this "simple" app, a `:timer` event is dispatched every second:
|
||||
```clj
|
||||
(defn dispatch-timer-event
|
||||
[]
|
||||
(let [now (js/Date.)]
|
||||
(rf/dispatch [:timer now]))) ;; <-- dispatch used
|
||||
|
||||
;; call the dispatching function every second
|
||||
(defonce do-timer (js/setInterval dispatch-timer-event 1000))
|
||||
```
|
||||
This is an unusual source of events. Normally, it is an app's UI widgets which
|
||||
`dispatch` events (in response to user actions), or an HTTP POST's
|
||||
`on-success` handler, or a websocket which gets a new packet.
|
||||
|
||||
### After dispatch
|
||||
|
||||
`dispatch` puts an event into a queue for processing.
|
||||
|
||||
So, **an event is not processed synchronously, like a function call**. The processing
|
||||
happens **later** - asynchronously. Very soon, but not now.
|
||||
|
||||
The consumer of the queue is a `router` which looks after the event's processing.
|
||||
|
||||
The `router`:
|
||||
|
||||
1. inspects the 1st element of an event vector
|
||||
2. looks in a registry for the event handler which is **registered**
|
||||
for this kind of event
|
||||
3. calls that event handler with the necessary arguments
|
||||
|
||||
As a re-frame app developer, your job, then, is to write and register a handler
|
||||
for each kind of event.
|
||||
|
||||
## Event Handlers (domino 2)
|
||||
|
||||
Collectively, event handlers provide the control logic in a re-frame application.
|
||||
|
||||
In this application, 3 kinds of event are dispatched:
|
||||
`:initialise`
|
||||
`:time-color-change`
|
||||
`:timer`
|
||||
|
||||
3 events means we'll be registering 3 event handlers.
|
||||
|
||||
### Two ways To register
|
||||
|
||||
Event handlers can be registered via either `reg-event-db`
|
||||
or `reg-event-fx` (`-db` vs `-fx`).
|
||||
|
||||
Handler functions take coeffects (input args) and return `effects`,
|
||||
however `reg-event-db` allows you to write simpler handlers.
|
||||
The
|
||||
handler functions it registers
|
||||
(1) take just one coeffect - the current app state and (2) return only one `effect` -
|
||||
the updated app state.
|
||||
|
||||
Whereas `reg-event-fx` registered handlers are more flexible.
|
||||
|
||||
Because of its simplicity, we'll be using the former here.
|
||||
|
||||
### reg-event-db
|
||||
|
||||
We register event handlers using re-frame's `reg-event-db`.
|
||||
|
||||
`reg-event-db` is used like this:
|
||||
```clj
|
||||
(reg-event-db
|
||||
:the-event-id
|
||||
the-event-handler-fn)
|
||||
```
|
||||
The handler function you provide should expect two parameters:
|
||||
- `db` the current application state (contents of `app-db`)
|
||||
- `v` the event vector
|
||||
|
||||
So, your function will have a signature like this: `(fn [db v] ...)`.
|
||||
|
||||
Each event handler must compute and return the new state of
|
||||
the application, which means it normally returns a
|
||||
modified version of `db`.
|
||||
|
||||
### :initialize
|
||||
|
||||
On startup, application state must be initialised. We
|
||||
want to put a sensible value into `app-db` which will
|
||||
otherwise contain `{}`.
|
||||
|
||||
So a `(dispatch [:initialize])` will happen early in the
|
||||
apps life (more on this below), and we need to write an `event handler`
|
||||
for it.
|
||||
|
||||
Now this event handler is slightly unusual because it doesn't
|
||||
much care about the existing value in `db` - it just wants to plonk
|
||||
in a new complete value.
|
||||
|
||||
Like this:
|
||||
```clj
|
||||
(rf/reg-event-db ;; sets up initial application state
|
||||
:initialize
|
||||
(fn [_ _] ;; the two parameters are not important here, so use _
|
||||
{:time (js/Date.) ;; What it returns becomes the new application state
|
||||
:time-color "#f88"})) ;; so the application state will initially be a map with two keys
|
||||
```
|
||||
|
||||
This particular handler `fn` ignores the two parameters
|
||||
(usually called `db` and `v`) and simply returns
|
||||
a map literal, which becomes the application
|
||||
state.
|
||||
|
||||
Here's an alternative way of writing it which does pay attention to the existing value of `db`:
|
||||
```clj
|
||||
(rf/reg-event-db
|
||||
:initialize
|
||||
(fn [db _] ;; we use db this time, so name it
|
||||
(-> db
|
||||
(assoc :time (js/Date.))
|
||||
(assoc :time-color "#f88")))
|
||||
```
|
||||
|
||||
|
||||
### :timer
|
||||
|
||||
Earlier, we set up a timer function to `(dispatch [:timer now])` every second.
|
||||
|
||||
Here's how we handle it:
|
||||
```clj
|
||||
(rf/reg-event-db ;; usage: (dispatch [:timer a-js-Date])
|
||||
:timer
|
||||
(fn [db [_ new-time]] ;; <-- de-structure the event vector
|
||||
(assoc db :time new-time))) ;; compute and return the new application state
|
||||
```
|
||||
|
||||
Notes:
|
||||
1. the `event` will be like `[:timer a-time]`, so the 2nd `v` parameter
|
||||
destructures to extract the `a-time` value
|
||||
2. the handler computes a new application state from `db`, and returns it
|
||||
|
||||
### :time-color-change
|
||||
|
||||
When the user enters a new colour value (via an input text box):
|
||||
```clj
|
||||
(rf/reg-event-db
|
||||
:time-color-change ;; usage: (dispatch [:time-color-change 34562])
|
||||
(fn [db [_ new-color-value]]
|
||||
(assoc db :time-color new-color-value))) ;; compute and return the new application state
|
||||
```
|
||||
|
||||
## Effect Handlers (domino 3)
|
||||
|
||||
Domino 3 actions/realises the `effects` returned by event handlers.
|
||||
|
||||
In this "simple" application, our event handlers are implicitly returning
|
||||
only one effect: "update application state".
|
||||
|
||||
This particular `effect` is actioned by a re-frame supplied
|
||||
`effect handler`. **So, there's nothing for us to do for this domino**. We are
|
||||
using a standard re-frame effect handler.
|
||||
|
||||
And this is not unusual. You'll seldom have to write `effect handlers`, but
|
||||
we'll understand more about them in a later tutorial.
|
||||
|
||||
## Subscription Handlers (domino 4)
|
||||
|
||||
Subscription handlers take application state as an argument,
|
||||
and they compute a query over it, returning something of
|
||||
a "materialised view" of that application state.
|
||||
|
||||
When the application state changes, subscription functions are
|
||||
re-run by re-frame, to compute new values (a new materialised view).
|
||||
|
||||
Ultimately, the data returned by `query` functions is used
|
||||
in the `view` functions (Domino 5).
|
||||
|
||||
One subscription can
|
||||
source data from other subscriptions. So it is possible to
|
||||
create a tree of dependencies.
|
||||
|
||||
The Views (Domino 5) are the leaves of this tree The tree's
|
||||
root is `app-db` and the intermediate nodes between the two
|
||||
are computations being performed by the query functions of Domino 4.
|
||||
|
||||
Now, the two examples below are trivial. They just extract part of the application
|
||||
state and return it. So, there's virtually no computation. A more interesting tree
|
||||
of subscriptions and more explanation can be found in the todomvc example.
|
||||
|
||||
### reg-sub
|
||||
|
||||
`reg-sub` associates a `query id` with a function that computes
|
||||
that query, like this:
|
||||
```clj
|
||||
(reg-sub
|
||||
:some-query-id ;; query id (used later in subscribe)
|
||||
a-query-fn) ;; the function which will compute the query
|
||||
```
|
||||
If, later, a view function subscribes to a query like this:
|
||||
`(subscribe [:some-query-id])` ;; note use of `:some-query-id`
|
||||
then `a-query-fn` will be used to perform the query over the application state.
|
||||
|
||||
Each time application state changes, `a-query-fn` will be
|
||||
called again to compute a new materialised view (a new computation over app state)
|
||||
and that new value will be given to any view function which is subscribed
|
||||
to `:some-query-id`. This view function, itself, will then also be called again
|
||||
to compute new DOM (because it depends on a query value which changed).
|
||||
|
||||
Along this reactive chain of dependencies, re-frame will ensure the
|
||||
necessary calls are made, at the right time.
|
||||
|
||||
Here's the code:
|
||||
```clj
|
||||
(rf/reg-sub
|
||||
:time
|
||||
(fn [db _] ;; db is current app state. 2nd unused param is query vector
|
||||
(:time db))) ;; return a query computation over the application state
|
||||
|
||||
(rf/reg-sub
|
||||
:time-color
|
||||
(fn [db _]
|
||||
(:time-color db)))
|
||||
```
|
||||
|
||||
Like I said, both of these queries are trivial. See `todomvc.subs.clj` for more interesting ones.
|
||||
|
||||
## View Functions (domino 5)
|
||||
|
||||
`view` functions turn data into DOM. They are "State in, Hiccup out" and they are Reagent components.
|
||||
|
||||
Any SPA will have lots of `view`functions, and collectively,
|
||||
they render the app's entire UI.
|
||||
|
||||
### Hiccup
|
||||
|
||||
`Hiccup` is a data format for representing HTML.
|
||||
|
||||
Here's a trivial view function which returns hiccup-formatted data:
|
||||
```clj
|
||||
(defn greet
|
||||
[]
|
||||
[:div "Hello viewers"]) ;; means <div>Hello viewers</div>
|
||||
```
|
||||
|
||||
And if we call it:
|
||||
```clj
|
||||
(greet)
|
||||
;; ==> [:div "Hello viewers"]
|
||||
|
||||
(first (greet))
|
||||
;; ==> :div
|
||||
```
|
||||
|
||||
Yep, that's a vector with two elements: a keyword and a string.
|
||||
|
||||
Now,`greet` is pretty simple because it only has the "Hiccup Out" part. There's no "Data In".
|
||||
|
||||
### Subscribing
|
||||
|
||||
To render the DOM representation of some-part-of app state, view functions must query
|
||||
for that part of `app-db`, and that means using `subscribe`.
|
||||
|
||||
`subscribe` is always called like this:
|
||||
```Clojure
|
||||
(subscribe [query-id some optional query parameters])
|
||||
```
|
||||
There's only one (global) `subscribe` function and it takes one argument, assumed to be a vector.
|
||||
|
||||
The first element in the vector (shown above as `query-id`) identifies/names the query
|
||||
and the other elements are optional
|
||||
query parameters. With a traditional database a query might be:
|
||||
```
|
||||
select * from customers where name="blah"
|
||||
```
|
||||
|
||||
In re-frame, that would be done as follows:
|
||||
`(subscribe [:customer-query "blah"])`
|
||||
which would return a `ratom` holding the customer state (a value which might change over time!).
|
||||
|
||||
> Because subscriptions return a ratom, they must always be dereferenced to
|
||||
obtain the value. This is a recurring trap for newbies.
|
||||
|
||||
### The View Functions
|
||||
|
||||
This view function renders the clock:
|
||||
```clj
|
||||
(defn clock
|
||||
[]
|
||||
[:div.example-clock
|
||||
{:style {:color @(rf/subscribe [:time-color])}}
|
||||
(-> @(rf/subscribe [:time])
|
||||
.toTimeString
|
||||
(clojure.string/split " ")
|
||||
first)])
|
||||
```
|
||||
As you can see, it uses `subscribe` twice to obtain two pieces of data from `app-db`.
|
||||
If either change, re-frame will re-run this view function.
|
||||
|
||||
And this view function renders the input field:
|
||||
```clj
|
||||
(defn color-input
|
||||
[]
|
||||
[:div.color-input
|
||||
"Time color: "
|
||||
[:input {:type "text"
|
||||
:value @(rf/subscribe [:time-color]) ;; subscribe
|
||||
:on-change #(rf/dispatch [:time-color-change (-> % .-target .-value)])}]]) ;; <---
|
||||
```
|
||||
|
||||
Notice how it does BOTH a `subscribe` to obtain the current value AND a `dispatch` to say when it has changed.
|
||||
|
||||
It is very common for view functions to render event-dispatching functions. The user's interaction with
|
||||
the UI is usually the largest source of events.
|
||||
|
||||
And then something more standard:
|
||||
```clj
|
||||
(defn ui
|
||||
[]
|
||||
[:div
|
||||
[:h1 "Hello world, it is now"]
|
||||
[clock]
|
||||
[color-input]])
|
||||
```
|
||||
|
||||
Note: `view` functions tend to be organised into a hierarchy, often with data flowing from parent to child via
|
||||
parameters. So, not every view function needs a subscription. Very often the values passed in from a parent component
|
||||
are sufficient.
|
||||
|
||||
Note: a view function should never directly access `app-db`. Data is only ever sourced via
|
||||
|
||||
### Components Like Templates?
|
||||
|
||||
`view` functions are like the templates you'd find in
|
||||
Django, Rails, Handlebars or Mustache -- they map data to HTML -- except for two massive differences:
|
||||
|
||||
1. you have the full power of ClojureScript available to you (generating a Clojure data structure). The
|
||||
downside is that these are not "designer friendly" HTML templates.
|
||||
2. these templates are reactive. When their input Signals change, they
|
||||
are automatically rerun, producing new DOM. Reagent adroitly shields you from the details, but
|
||||
the renderer of any `component` is wrapped by a `reaction`. If any of the the "inputs"
|
||||
to that render change, the render is rerun.
|
||||
|
||||
## Kick Starting The App
|
||||
|
||||
Below, `run` is the called when the HTML page has loaded
|
||||
to kick off the application.
|
||||
|
||||
It has two tasks:
|
||||
1. load the initial application state
|
||||
2. "mount" the GUI onto an existing DOM element.
|
||||
|
||||
```clj
|
||||
(defn ^:export run
|
||||
[]
|
||||
(rf/dispatch-sync [:initialize]) ;; puts a value into application state
|
||||
(reagent/render [ui] ;; mount the application's ui into '<div id="app" />'
|
||||
(js/document.getElementById "app")))
|
||||
```
|
||||
|
||||
After `run` is called, the app passively waits for events.
|
||||
Nothing happens without an `event`.
|
||||
|
||||
When it comes to establishing initial application state, you'll
|
||||
notice the use of `dispatch-sync`, rather than `dispatch`. This is something of
|
||||
cheat which ensures a correct
|
||||
structure exists in `app-db` before any subscriptions or event handlers run.
|
||||
|
||||
## Summary
|
||||
|
||||
**Your job**, when building an app, is to:
|
||||
- design your app's information model (data and schema layer)
|
||||
- write and register event handler functions (control and transition layer) (domino 2)
|
||||
- (once in a blue moon) write and register effect and coeffect handler
|
||||
functions (domino 3) which do the mutative dirty work of which we dare not
|
||||
speak in a pure, immutable functional context. Most of the time, you'll be
|
||||
using standard, supplied ones.
|
||||
- write and register query functions which implement nodes in a signal graph (query layer) (domino 4)
|
||||
- write Reagent view functions (view layer) (domino 5)
|
||||
|
||||
## Further Code
|
||||
|
||||
You should also look at the [todomvc example application](https://github.com/Day8/re-frame/tree/develop/examples/todomvc).
|
||||
|
||||
|
||||
***
|
||||
|
||||
Previous: [app-db (Application State)](ApplicationState.md)
|
||||
Up: [Index](README.md)
|
||||
Next: [Mental Model Omnibus](MentalModelOmnibus.md)
|
||||
|
|
@ -4,24 +4,26 @@ This tutorial explains `coeffects`.
|
|||
|
||||
It explains what they are, how they can be "injected", and how
|
||||
to manage them in tests.
|
||||
|
||||
## Table Of Contexts
|
||||
|
||||
* [What Are They?](#what-are-they-)
|
||||
* [An Example](#an-example)
|
||||
* [How We Want It](#how-we-want-it)
|
||||
* [Abracadabra](#abracadabra)
|
||||
* [Which Interceptors?](#which-interceptors-)
|
||||
* [`inject-cofx`](#-inject-cofx-)
|
||||
* [More `inject-cofx`](#more--inject-cofx-)
|
||||
* [Meet `reg-cofx`](#meet--reg-cofx-)
|
||||
* [Example Of `reg-cofx`](#example-of--reg-cofx-)
|
||||
* [Another Example Of `reg-cofx`](#another-example-of--reg-cofx-)
|
||||
* [Secret Interceptors](#secret-interceptors)
|
||||
* [Testing](#testing)
|
||||
* [The 5 Point Summary](#the-5-point-summary)
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
## Coeffects
|
||||
- [What Are They?](#what-are-they)
|
||||
- [An Example](#an-example)
|
||||
- [How We Want It](#how-we-want-it)
|
||||
- [Abracadabra](#abracadabra)
|
||||
- [Which Interceptors?](#which-interceptors)
|
||||
- [`inject-cofx`](#inject-cofx)
|
||||
- [More `inject-cofx`](#more-inject-cofx)
|
||||
- [Meet `reg-cofx`](#meet-reg-cofx)
|
||||
- [Example Of `reg-cofx`](#example-of-reg-cofx)
|
||||
- [Another Example Of `reg-cofx`](#another-example-of-reg-cofx)
|
||||
- [Secret Interceptors](#secret-interceptors)
|
||||
- [Testing](#testing)
|
||||
- [The 5 Point Summary](#the-5-point-summary)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
### What Are They?
|
||||
|
||||
|
@ -30,7 +32,7 @@ to perform its computation.
|
|||
|
||||
Because the majority of event handlers only need `db` and
|
||||
`event`, there's a specific registration function, called `reg-event-db`,
|
||||
which delivers these two coeffects as arguments to an event
|
||||
which delivers ONLY these two coeffects as arguments to an event
|
||||
handler, making this common case easy to program.
|
||||
|
||||
But sometimes an event handler needs other data inputs
|
||||
|
@ -60,7 +62,7 @@ pure, and impure functions cause well-documented paper cuts.
|
|||
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.
|
||||
|
||||
The first is that we first switch to
|
||||
The first is that we switch to
|
||||
using `reg-event-fx` (instead of `reg-event-db`).
|
||||
|
||||
Event handlers registered via `reg-event-fx` are slightly
|
||||
|
@ -86,22 +88,22 @@ 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`
|
||||
is created, and within that `context` is a brand new `:coeffects`
|
||||
map, which is initially totally empty.
|
||||
|
||||
That pristine `context` value (containing a pristine `:coeffect` map) is threaded
|
||||
That pristine `context` value (containing a pristine `:coeffects` map) is threaded
|
||||
through a chain of Interceptors before it finally reaches our event handler,
|
||||
sitting on the end of a chain, itself wrapped up in an interceptor. We know
|
||||
this story well from a previous tutorial.
|
||||
|
||||
So, all members of the Interceptor chain have the opportunity to add to `:coeffects`
|
||||
via their `:before` function. This is where `:coeffect` magic happens. This is how
|
||||
new keys can be added to `:coeffect`, so that later our event handler magically finds the
|
||||
via their `:before` function. This is where `:coeffects` magic happens. This is how
|
||||
new keys can be added to `:coeffects`, so that later our event handler magically finds the
|
||||
right data (like `:local-store`) in its `cofx` argument. It is the Interceptors.
|
||||
|
||||
### Which Interceptors?
|
||||
|
||||
If Interceptors put data in `:coeffect`, then we'll need to add the right ones
|
||||
If Interceptors put data in `:coeffects`, then we'll need to add the right ones
|
||||
when we register our event handler.
|
||||
|
||||
Something like this (this handler is the same as before, except for one detail):
|
||||
|
@ -125,7 +127,7 @@ to our event handler (`cofx`).
|
|||
`inject-cofx` is part of the re-frame API.
|
||||
|
||||
It is a function which returns an Interceptor whose `:before` function loads
|
||||
a key/value pair into a `context's` `:coeffect` map.
|
||||
a key/value pair into a `context's` `:coeffects` map.
|
||||
|
||||
`inject-cofx` takes either one or two arguments. The first is always the `id` of the coeffect
|
||||
required (called a `cofx-id`). The 2nd is an optional addition value.
|
||||
|
@ -160,7 +162,7 @@ Each `cofx-id` requires a different action.
|
|||
|
||||
This function is also part of the re-frame API.
|
||||
|
||||
It allows you to associate a`cofx-id` (like `:now` or `:local-store`) with a
|
||||
It allows you to associate a `cofx-id` (like `:now` or `:local-store`) with a
|
||||
handler function that injects the right key/value pair.
|
||||
|
||||
The function you register will be passed two arguments:
|
||||
|
@ -267,7 +269,8 @@ In note form:
|
|||
5. We must have previously registered a cofx handler via `reg-cofx`
|
||||
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
Previous: [Effects](Effects.md)
|
||||
Up: [Index](README.md)
|
||||
Next: [Basic App Structure](Basic-App-Structure.md)
|
||||
|
|
|
@ -6,42 +6,52 @@ Event handlers are quite central to a re-frame app. Only event handlers
|
|||
can update `app-db`, to "step" an application "forward" from one state
|
||||
to the next.
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [The `debug` Interceptor](#the-debug-interceptor)
|
||||
- [Using `debug`](#using-debug)
|
||||
- [Too Much Repetition - Part 1](#too-much-repetition---part-1)
|
||||
- [3. Checking DB Integrity](#3-checking-db-integrity)
|
||||
- [Too Much Repetition - Part 2](#too-much-repetition---part-2)
|
||||
- [What about the -fx variation?](#what-about-the--fx-variation)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## The `debug` Interceptor
|
||||
|
||||
You might wonder: is my handler making the right changes to the
|
||||
value in `app-db`? Does it remove that entry? Does it increment that
|
||||
value?
|
||||
You might wonder: is my event handler making the right changes to `app-db`?
|
||||
|
||||
During development, the built-in `debug` interceptor can be helpful
|
||||
in this regard. It shows, via `console.log`:
|
||||
During development, the built-in `debug` interceptor can help.
|
||||
It writes to `console.log`:
|
||||
1. the event being processed, for example: `[:attempt-world-record true]`
|
||||
2. the changes made to `db` by the handler in processing the event.
|
||||
2. the changes made to `db` by the handler in processing the event
|
||||
|
||||
Regarding point 2, `debug` uses `clojure.data/diff` to compare the
|
||||
state of `db` before and after the handler ran, showing exactly what
|
||||
mutation has happened.
|
||||
`debug` uses `clojure.data/diff` to compare `app-db`
|
||||
before and after the handler ran, showing what changed.
|
||||
|
||||
If you [look at the docs for diff](https://clojuredocs.org/clojure.data/diff),
|
||||
you'll notice it returns a triple, the first two of which
|
||||
`debug` will display in `console.log` (the 3rd says what hasn't changed
|
||||
and isn't interesting).
|
||||
[clojure.data/diff returns a triple](https://clojuredocs.org/clojure.data/diff)
|
||||
, the first two entries of which
|
||||
`debug` will display in `console.log` (the 3rd says what hasn't changed and isn't interesting).
|
||||
|
||||
The output produced by `clojure.data/diff` can take some getting used to,
|
||||
but you should stick with it -- your effort will be rewarded.
|
||||
|
||||
### Using `debug`
|
||||
|
||||
So, you will add this interceptor to your event handlers like this:
|
||||
So, you will add this Interceptor like this:
|
||||
```clj
|
||||
(re-frame.core/reg-event-db
|
||||
:some-id
|
||||
[debug] ;; <---- here!
|
||||
[debug] ;; <---- added here!
|
||||
some-handler-fn)
|
||||
```
|
||||
|
||||
Except, of course, we need a bit more subtly than that because
|
||||
we only want `debug` to be present in development builds.
|
||||
So it should be like this:
|
||||
Except, of course, we need to be more deft - we only want
|
||||
`debug` in development builds. We don't
|
||||
want the overhead of those `clojure.data/diff` calculations in production.
|
||||
So, this is better:
|
||||
```clj
|
||||
(re-frame.core/reg-event-db
|
||||
:some-id
|
||||
|
@ -54,38 +64,58 @@ It will be `true` when the build within `project.clj` is `:optimization :none` a
|
|||
otherwise.
|
||||
|
||||
Ha! I see a problem, you say. In production, that `when` is going to
|
||||
leave a `nil` in the interceptor vector. No problem. re-frame filters out nils.
|
||||
leave a `nil` in the interceptor vector. So the Interceptor vector will be `[nil]`.
|
||||
Surely that's a problem?
|
||||
|
||||
Well, actually, no it isn't. re-frame filters out `nil`.
|
||||
|
||||
### Too Much Repetition - Part 1
|
||||
|
||||
Remember that each event handler has its own interceptor stack.
|
||||
All very flexible, but does that mean we have to repeat this `debug`
|
||||
business on every single handler? Yes, it does. But there are
|
||||
a couple of ways to make this pretty easy.
|
||||
Each event handler has its own interceptor stack.
|
||||
|
||||
Normally, standard interceptors are defined up the top of the `event.cljs` namespace:
|
||||
That might be all very flexible, but does that mean we have to put this `debug`
|
||||
business on every single handler? That would be very repetitive.
|
||||
|
||||
Yes, you will have to put it on each handler. And, yes, that could be repetitive, unless
|
||||
you take some steps.
|
||||
|
||||
One thing you an do is to define standard interceptors the top of the `event.cljs` namespace:
|
||||
```clj
|
||||
(def standard-interceptors [(when ^boolean goog.DEBUG debug) other-interceptor])
|
||||
(def standard-interceptors [(when ^boolean goog.DEBUG debug) another-interceptor])
|
||||
```
|
||||
|
||||
And then, any one event handler, would look like:
|
||||
And then, for any one event handler, the code would look like:
|
||||
```clj
|
||||
(re-frame.core/reg-event-db
|
||||
:some-id
|
||||
[standard-interceptors specific-interceptor]
|
||||
standard-interceptors ;; <--- use the common definition
|
||||
some-handler-fn)
|
||||
```
|
||||
|
||||
Wait on! I see a problem, you say. `standard-interceptors` is a `vector`, and it
|
||||
is within another `vector` allongside `specific-interceptor` - so that's
|
||||
or perhaps:
|
||||
```clj
|
||||
(re-frame.core/reg-event-db
|
||||
:some-id
|
||||
[standard-interceptors specific-interceptor] ;; mix with something specific
|
||||
some-handler-fn)
|
||||
```
|
||||
|
||||
So that `specific-interceptor` could be something required for just this one
|
||||
event handler, and it can be combined the standard ones.
|
||||
|
||||
Wait on! "I see a problem", you say. `standard-interceptors` is a `vector`, and it
|
||||
is within another `vector` along side `specific-interceptor` - so that's
|
||||
nested vectors of interceptors!
|
||||
|
||||
No problem, re-frame uses `flatten` to take out all the nesting - the
|
||||
result is a simple chain of interceptors. Also, of course, nils are removed.
|
||||
result is a simple chain of interceptors. And also, as we have discussed,
|
||||
nils are removed.
|
||||
|
||||
## 3. Checking DB Integrity
|
||||
|
||||
Always have a detailed schema for the data in `app-db`.
|
||||
Always have a detailed schema for the data in `app-db`!
|
||||
|
||||
Why?
|
||||
|
||||
**First**, schemas serve as invaluable documentation. When I come to
|
||||
a new app, the first thing I want to look at is the underlying
|
||||
|
@ -96,15 +126,20 @@ or, perhaps, [a Prismatic Schema](https://github.com/Prismatic/schema).
|
|||
|
||||
|
||||
**Second** a good spec allows you to assert the integrity and correctness of
|
||||
the data in app-db.
|
||||
the data in `app-db`. Because all the data is in one place, that means you
|
||||
are asserting the integrity of ALL the data in your app, at one time.
|
||||
|
||||
When? Well, only event handlers can change what's in `app-db`, so only an event handler
|
||||
could corrupt it. So, we'd like to recheck the integrity of `app-db` immediately
|
||||
after **every** event handler has run.
|
||||
When should we do this? Ideally every time a change is made!
|
||||
|
||||
This allows us to catch any errors very early, and easily assign blame (to an event handler).
|
||||
Well, it turns out that only event handlers can change the value in
|
||||
`app-db`, so only an event handler could corrupt it. So, we'd like to
|
||||
**recheck the integrity of `app-db` immediately
|
||||
after *every* event handler has run**.
|
||||
|
||||
Schemas are typically put into `db.cljs`. Here's an example using Prismatic Schema
|
||||
This allows us to catch any errors very early, easily assigning blame (to the rouge event handler).
|
||||
|
||||
Schemas are typically put into `db.cljs` (see the todomvc example in the re-frame repo). Here's
|
||||
an example using Prismatic Schema
|
||||
(although a more modern choice would be to use [Clojure spec](http://clojure.org/about/spec)):
|
||||
```clj
|
||||
(ns my.namespace.db
|
||||
|
@ -119,7 +154,10 @@ Schemas are typically put into `db.cljs`. Here's an example using Prismatic Sche
|
|||
:c s/Int}
|
||||
:d [{:e s/Keyword
|
||||
:f [s/Num]}]})
|
||||
```
|
||||
|
||||
And a function which will check a db value against that schema:
|
||||
```clj
|
||||
(defn valid-schema?
|
||||
"validate the given db, writing any problems to console.error"
|
||||
[db]
|
||||
|
@ -128,19 +166,21 @@ Schemas are typically put into `db.cljs`. Here's an example using Prismatic Sche
|
|||
(.error js/console (str "schema problem: " res)))))
|
||||
```
|
||||
|
||||
Now, let's organise for `valid-schema?` to be run after every handler. We'll use the built-in `after` interceptor factory:
|
||||
Now, let's organise for `valid-schema?` to be run **after** every handler.
|
||||
We'll use the built-in `after` Interceptor factory function:
|
||||
```clj
|
||||
(def standard-interceptors [(when ^boolean goog.DEBUG debug)
|
||||
(when ^boolean goog.DEBUG (after db/valid-schema?))]) ;; <-- new
|
||||
```
|
||||
|
||||
|
||||
Now, the instant a handler messes up the structure of `app-db` you'll be alerted. But this overhead won't be there in production.
|
||||
|
||||
### Too Much Repetition - Part 2
|
||||
|
||||
Above we discussed a way of "factoring out" common interceptors into `standard-interceptors`.
|
||||
But there's a 2nd way to ensure that all event handlers get certain Interceptors: you write a custom registration function, like this:
|
||||
|
||||
But there's a 2nd way to ensure that all event handlers get certain Interceptors:
|
||||
you write a custom registration function -- a replacement for `reg-event-db` -- like this:
|
||||
```clj
|
||||
(defn my-reg-event-db ;; alternative to reg-event-db
|
||||
([id handler-fn]
|
||||
|
@ -154,9 +194,37 @@ But there's a 2nd way to ensure that all event handlers get certain Interceptors
|
|||
handler-fn)))
|
||||
```
|
||||
|
||||
From now on, you can register your event handlers like this:
|
||||
Notice that it inserts our two standard Interceptors.
|
||||
|
||||
From now on, you can register your event handlers like this and know that the two standard Interceptors have been inserted:
|
||||
```clj
|
||||
(my-reg-event-db ;; <-- adds std interceptors automatically
|
||||
:some-id
|
||||
some-handler-fn)
|
||||
```
|
||||
|
||||
### What about the -fx variation?
|
||||
|
||||
Above we created `my-reg-event-db` as a new registration function for `-db` handlers.
|
||||
That's handlers which take `db` and `event` arguments, and return a new `db`.
|
||||
So, they MUST return a new `db` value - which should be validated.
|
||||
|
||||
But what if we tried to do the same for `-fx` handlers, which return instead
|
||||
an `effects` map which may, or may not, contain an `:db`? Our solution would
|
||||
have to allow for the absence of a new `db` value (by doing no validity check, because nothing
|
||||
was being changed).
|
||||
|
||||
```clj
|
||||
(defn my-reg-event-fx ;; alternative to reg-event-db
|
||||
([id handler-fn]
|
||||
(my-reg-event-db id nil handler-fn))
|
||||
([id interceptors handler-fn]
|
||||
(re-frame.core/reg-event-fx
|
||||
id
|
||||
[(when ^boolean goog.DEBUG debug)
|
||||
(when ^boolean goog.DEBUG (after #(if % (db/valid-schema? %))))
|
||||
interceptors]
|
||||
handler-fn)))
|
||||
```
|
||||
|
||||
Actually, it would probably be better to write an alternative `after` which
|
||||
|
|
|
@ -2,7 +2,26 @@
|
|||
|
||||
This page describes a technique for
|
||||
debugging re-frame apps. It proposes a particular combination
|
||||
of tools. By the end, you'll be better at dominos.
|
||||
of tools.
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Know The Beast!](#know-the-beast)
|
||||
- [re-frame's Step 3](#re-frames-step-3)
|
||||
- [Observe The Beast](#observe-the-beast)
|
||||
- [How To Trace?](#how-to-trace)
|
||||
- [Your browser](#your-browser)
|
||||
- [Your Project](#your-project)
|
||||
- [Say No To Anonymous](#say-no-to-anonymous)
|
||||
- [IMPORTANT](#important)
|
||||
- [The result](#the-result)
|
||||
- [Warning](#warning)
|
||||
- [React Native](#react-native)
|
||||
- [Appendix A - Prior to V0.8.0](#appendix-a---prior-to-v080)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Know The Beast!
|
||||
|
||||
|
@ -59,8 +78,8 @@ as luck would have it, ClojureScript is a lisp and it is readily **traceable**.
|
|||
## How To Trace?
|
||||
|
||||
Below, I suggest a particular combination of technologies which, working together,
|
||||
will write a trace to the devtools console. Sorry, but there's no fancy
|
||||
SVG dashboard. We said simple, right?
|
||||
will write a trace to the devtools console. Sorry, but there's no fancy
|
||||
SVG dashboard. We said simple, right?
|
||||
|
||||
First, use clairvoyant to trace function calls and data flow. We've had
|
||||
a couple of Clairvoyant PRs accepted, and they make it work well for us.
|
||||
|
@ -248,8 +267,156 @@ From @mccraigmccraig we get the following (untested by me, but they look great):
|
|||
> I finally had enough of all the boilerplate required to use clairvoyant with
|
||||
> re-frame subs & handlers and wrote some code to tidy it up...
|
||||
|
||||
> https://www.refheap.com/799f89796b078b6e2459df038
|
||||
```clj
|
||||
(ns er-webui.re-frame
|
||||
(:require
|
||||
[clojure.string :as str]
|
||||
[clojure.pprint :as pp]
|
||||
[clairvoyant.core]
|
||||
[cljs.analyzer :as analyzer]))
|
||||
|
||||
> gives you subs like this - https://www.refheap.com/e80f7f982f2bf75bd36bb1062
|
||||
(def expand-macros
|
||||
#{`reaction
|
||||
`regsub
|
||||
`reghandler})
|
||||
|
||||
> and handlers like this - https://www.refheap.com/e6a6a3a78eb768de386f54b49
|
||||
(defn expand-op?
|
||||
"should the op represented by the sym be expanded...
|
||||
expands the sym to its fully namespaced version and
|
||||
checks against expand-macros"
|
||||
[sym env]
|
||||
(when-let [{var-name :name} (analyzer/resolve-macro-var env sym)]
|
||||
;; (pp/pprint ["expand-op?" sym var-name] *err*)
|
||||
(expand-macros var-name)))
|
||||
|
||||
(defn maybe-expand
|
||||
"recursively descend forms calling macroexpand-1
|
||||
on any forms with a symbol from expand-macros in
|
||||
first position"
|
||||
[form env]
|
||||
(if (and (seq? form)
|
||||
(symbol? (first form)))
|
||||
(let [[op & r] form
|
||||
resolved-op (expand-op? op env)]
|
||||
(if resolved-op
|
||||
(maybe-expand
|
||||
(macroexpand-1 (cons resolved-op r))
|
||||
env)
|
||||
(cons op
|
||||
(doall (for [f r]
|
||||
(maybe-expand
|
||||
f
|
||||
env))))))
|
||||
form))
|
||||
|
||||
(defn maybe-expand-forms
|
||||
[forms env]
|
||||
(doall
|
||||
(for [form forms]
|
||||
(let [exp (maybe-expand form env)]
|
||||
(when (not= exp form)
|
||||
;; (pp/pprint exp *err*)
|
||||
)
|
||||
exp))))
|
||||
|
||||
(defn fn-name
|
||||
"make a sensible fn name from
|
||||
a possibly namespaced symbol or keyword"
|
||||
([k] (fn-name k ""))
|
||||
([k suffix]
|
||||
(assert (or (keyword? k) (symbol? k)))
|
||||
(-> k
|
||||
(str suffix)
|
||||
(str/replace #"^:" "")
|
||||
(str/replace #"\." "-")
|
||||
(str/replace "/" "--")
|
||||
symbol)))
|
||||
|
||||
(defmacro reaction
|
||||
"like reagent.core/reaction except it gives the fn a name
|
||||
which makes for useful tracing"
|
||||
[reaction-name & body]
|
||||
(let [reaction-fn-name# (fn-name reaction-name)]
|
||||
`(reagent.ratom/make-reaction
|
||||
(~'fn ~reaction-fn-name#
|
||||
[]
|
||||
~@body))))
|
||||
|
||||
(defmacro regsub
|
||||
"like re-frame.core/register-sub except it creates
|
||||
the fn with a name for better tracing"
|
||||
[sub-key params & body]
|
||||
(assert (vector? params))
|
||||
(let [sub-fn-name# (fn-name sub-key)]
|
||||
`(re-frame.core/register-sub
|
||||
~sub-key
|
||||
(~'fn ~sub-fn-name#
|
||||
~params
|
||||
~@body))))
|
||||
|
||||
(defmacro reghandler
|
||||
"like re-frame.core/register-handler except it
|
||||
creates an fn with a name which makes for better tracing"
|
||||
[handler-key middleware-or-params & body]
|
||||
(let [handler-fn-name (fn-name handler-key "-h")
|
||||
middleware (when (and (not (vector? middleware-or-params))
|
||||
(vector? (first body)))
|
||||
middleware-or-params)
|
||||
params (if middleware
|
||||
(first body)
|
||||
middleware-or-params)
|
||||
body (if middleware
|
||||
(rest body)
|
||||
body)]
|
||||
(assert (vector? params))
|
||||
`(re-frame.core/register-handler
|
||||
~handler-key
|
||||
~middleware
|
||||
(~'fn ~handler-fn-name
|
||||
~params
|
||||
~@body))))
|
||||
|
||||
(defmacro trace-subs
|
||||
[& body]
|
||||
(let [body-forms# (maybe-expand-forms body &env)]
|
||||
`(clairvoyant.core/trace-forms
|
||||
{:tracer (re-frame-tracer.core/tracer :color "brown")}
|
||||
|
||||
~@body-forms#)))
|
||||
|
||||
(defmacro trace-handlers
|
||||
[& body]
|
||||
(let [body-forms# (maybe-expand-forms body &env)]
|
||||
`(clairvoyant.core/trace-forms
|
||||
{:tracer (re-frame-tracer.core/tracer :color "blue")}
|
||||
|
||||
~@body-forms#)))
|
||||
|
||||
(defmacro trace-views
|
||||
[& body]
|
||||
(let [body-forms# (maybe-expand-forms body &env)]
|
||||
`(clairvoyant.core/trace-forms
|
||||
{:tracer (re-frame-tracer.core/tracer :color "green")}
|
||||
|
||||
~@body-forms#)))
|
||||
```
|
||||
|
||||
> gives you subs like this -
|
||||
|
||||
```clj
|
||||
(regsub :initialised
|
||||
[db _]
|
||||
(reaction initialised-r
|
||||
(get-in @db [:initialised])))
|
||||
```
|
||||
|
||||
> and handlers like this -
|
||||
|
||||
```clj
|
||||
(reghandler
|
||||
:after-init
|
||||
er-middleware
|
||||
[db [_]]
|
||||
(code-push/sync)
|
||||
db)
|
||||
```
|
||||
|
|
|
@ -3,34 +3,37 @@
|
|||
This tutorial shows you how to implement pure event handlers that side-effect.
|
||||
Yes, a surprising claim.
|
||||
|
||||
## Table Of Contents
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
* [Events Happen](#events-happen)
|
||||
* [Handling The Happening](#handling-the-happening)
|
||||
* [Your Handling](#your-handling)
|
||||
* [90% Solution](#90--solution)
|
||||
* [Bad, Why?](#bad--why-)
|
||||
* [The 2nd Kind Of Problem](#the-2nd-kind-of-problem)
|
||||
* [Effects And Coeffects](#effects-and-coeffects)
|
||||
* [Why Does This Happen?](#why-does-this-happen-)
|
||||
* [Doing vs Causing](#doing-vs-causing)
|
||||
* [Et tu, React?](#et-tu--react-)
|
||||
* [Pattern Structure](#pattern-structure)
|
||||
* [Effects: The Two Step Plan](#effects--the-two-step-plan)
|
||||
* [Step 1 Of Plan](#step-1-of-plan)
|
||||
* [Another Example](#another-example)
|
||||
* [The Coeffects](#the-coeffects)
|
||||
* [Variations On A Theme](#variations-on-a-theme)
|
||||
* [Summary](#summary)
|
||||
- [Events Happen](#events-happen)
|
||||
- [Handling The Happening](#handling-the-happening)
|
||||
- [Your Handling](#your-handling)
|
||||
- [90% Solution](#90%25-solution)
|
||||
- [Bad, Why?](#bad-why)
|
||||
- [The 2nd Kind Of Problem](#the-2nd-kind-of-problem)
|
||||
- [Effects And Coeffects](#effects-and-coeffects)
|
||||
- [Why Does This Happen?](#why-does-this-happen)
|
||||
- [Doing vs Causing](#doing-vs-causing)
|
||||
- [Et tu, React?](#et-tu-react)
|
||||
- [Pattern Structure](#pattern-structure)
|
||||
- [Effects: The Two Step Plan](#effects-the-two-step-plan)
|
||||
- [Step 1 Of Plan](#step-1-of-plan)
|
||||
- [Another Example](#another-example)
|
||||
- [The Coeffects](#the-coeffects)
|
||||
- [Variations On A Theme](#variations-on-a-theme)
|
||||
- [Summary](#summary)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Effects
|
||||
### Events Happen
|
||||
|
||||
Events "happen" when they are dispatched.
|
||||
|
||||
So, this makes an event happen:
|
||||
```clj
|
||||
(dispatch [:set-flag true])
|
||||
(dispatch [:repair-ming-vase true])
|
||||
```
|
||||
|
||||
Events are normally triggered by an external agent: the user clicks a button, or a server-pushed
|
||||
|
@ -364,14 +367,14 @@ Just to be clear, the following two handlers achieve the same thing:
|
|||
```clj
|
||||
(reg-event-db
|
||||
:set-flag
|
||||
(fn [db [_ new-value]
|
||||
(fn [db [_ new-value]]
|
||||
(assoc db :flag new-value)))
|
||||
```
|
||||
vs
|
||||
```clj
|
||||
(reg-event-fx
|
||||
:set-flag
|
||||
(fn [cofx [_ new-value]
|
||||
(fn [cofx [_ new-value]]
|
||||
{:db (assoc (:db cofx) :flag new-value)}))
|
||||
```
|
||||
|
||||
|
@ -392,7 +395,9 @@ 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.
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
Previous: [Infographic Overview](EventHandlingInfographic.md)
|
||||
Up: [Index](README.md)
|
||||
Next: [Interceptors](Interceptors.md)
|
||||
|
||||
|
|
|
@ -10,24 +10,30 @@ make side effects a noop in event replays.
|
|||
> -- @stuarthalloway
|
||||
|
||||
|
||||
## Table Of Contexts
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
* [Where Effects Come From](#where-effects-come-from)
|
||||
* [The Effects Map](#the-effects-map)
|
||||
* [Infinite Effects](#infinite-effects)
|
||||
* [Extensible Side Effects](#extensible-side-effects)
|
||||
* [Writing An Effect Handler](#writing-an-effect-handler)
|
||||
* [:db Not Always Needed](#-db-not-always-needed)
|
||||
* [What Makes This Work?](#what-makes-this-work-)
|
||||
* [Order Of Effects?](#order-of-effects-)
|
||||
* [Effects With No Data](#effects-with-no-data)
|
||||
* [Testing And Noops](#testing-and-noops)
|
||||
* [Builtin Effect Handlers](#builtin-effect-handlers)
|
||||
+ [:dispatch-later](#-dispatch-later)
|
||||
+ [:dispatch](#-dispatch)
|
||||
+ [:dispatch-n](#-dispatch-n)
|
||||
+ [:deregister-event-handler](#-deregister-event-handler)
|
||||
+ [:db](#-db)
|
||||
- [Where Effects Come From](#where-effects-come-from)
|
||||
- [The Effects Map](#the-effects-map)
|
||||
- [Infinite Effects](#infinite-effects)
|
||||
- [Extensible Side Effects](#extensible-side-effects)
|
||||
- [Writing An Effect Handler](#writing-an-effect-handler)
|
||||
- [:db Not Always Needed](#db-not-always-needed)
|
||||
- [What Makes This Work?](#what-makes-this-work)
|
||||
- [Order Of Effects?](#order-of-effects)
|
||||
- [Effects With No Data](#effects-with-no-data)
|
||||
- [Testing And Noops](#testing-and-noops)
|
||||
- [Summary](#summary)
|
||||
- [Builtin Effect Handlers](#builtin-effect-handlers)
|
||||
- [:dispatch-later](#dispatch-later)
|
||||
- [:dispatch](#dispatch)
|
||||
- [:dispatch-n](#dispatch-n)
|
||||
- [:deregister-event-handler](#deregister-event-handler)
|
||||
- [:db](#db)
|
||||
- [External Effects](#external-effects)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
### Where Effects Come From
|
||||
|
||||
|
@ -283,13 +289,6 @@ registered handlers) to which you can return.
|
|||
XXX
|
||||
|
||||
|
||||
---
|
||||
Previous: [Interceptors](Interceptors.md)
|
||||
Up: [Index](Readme.md)
|
||||
Next: [Coeffects](Coeffects.md)
|
||||
|
||||
---
|
||||
|
||||
### Builtin Effect Handlers
|
||||
|
||||
#### :dispatch-later
|
||||
|
@ -307,7 +306,7 @@ Which means: in 200ms do this: `(dispatch [:event-id "param"])` and in 100ms ...
|
|||
|
||||
#### :dispatch
|
||||
|
||||
`dispatch` one event. Excepts a single vector.
|
||||
`dispatch` one event. Expects a single vector.
|
||||
|
||||
usage:
|
||||
```clj
|
||||
|
@ -351,12 +350,16 @@ usage:
|
|||
- https://github.com/Day8/re-frame-http-fx (GETs and POSTs)
|
||||
- https://github.com/Day8/re-frame-forward-events-fx (slightly exotic)
|
||||
- https://github.com/Day8/re-frame-async-flow-fx (more complicated)
|
||||
- https://github.com/micmarsh/re-frame-youtube-fx (YouTube iframe API wrapper)
|
||||
- https://github.com/madvas/re-frame-web3-fx (Ethereum Web3 API)
|
||||
- https://github.com/madvas/re-frame-google-analytics-fx (Google Analytics API)
|
||||
|
||||
Create a PR to include yours in this list.
|
||||
|
||||
XXX maybe put this list into the Wiki, so editable by all.
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
Previous: [Interceptors](Interceptors.md)
|
||||
Up: [Index](README.md)
|
||||
Next: [Coeffects](Coeffects.md)
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
## Event Handling Infographics
|
||||
|
||||
Three diagrams are provided:
|
||||
- a beginners romp
|
||||
- a useful, intermediate schematic depiction
|
||||
- an advanced, accurate and detailed view
|
||||
|
||||
They should be reviewed in conjunction with a reading of the written tutorials.
|
||||
|
||||
<img src="/images/event-handlers.png?raw=true">
|
||||
|
||||
***
|
||||
|
||||
|
||||
Up: [Index](README.md)
|
||||
Next: [Effectful Handlers](EffectfulHandlers.md)
|
|
@ -2,6 +2,16 @@
|
|||
|
||||
Templates, examples, and other resources related to re-frame. For additions or modifications, please create an issue with a link and description or submit a pull request.
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Templates](#templates)
|
||||
- [Examples and Applications Using re-frame](#examples-and-applications-using-re-frame)
|
||||
- [Videos](#videos)
|
||||
- [Server Side Rendering](#server-side-rendering)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
### Templates
|
||||
|
||||
|
@ -18,6 +28,8 @@ Templates, examples, and other resources related to re-frame. For additions or m
|
|||
|
||||
### Examples and Applications Using re-frame
|
||||
|
||||
* [How to create decentralised apps with re-frame and Ethereum](https://medium.com/@matus.lestan/how-to-create-decentralised-apps-with-clojurescript-re-frame-and-ethereum-81de24d72ff5#.b9xh9xnis) by [Matus Lestan] - Tutorial with links to code and live example. Based on re-frame `0.8.0`
|
||||
|
||||
* [Elfeed-cljsrn](https://github.com/areina/elfeed-cljsrn) by [Toni Reina] - A mobile client for [Elfeed](https://github.com/skeeto/elfeed) rss reader, built with React Native. Based on re-frame `0.8.0`
|
||||
|
||||
* [Memory Hole](https://github.com/yogthos/memory-hole) by [Yogthos] - A small issue tracking app written with Luminus and re-frame. Based on re-frame `0.8.0`
|
||||
|
@ -35,6 +47,7 @@ Templates, examples, and other resources related to re-frame. For additions or m
|
|||
|
||||
* [Angular Phonecat tutorial in re-frame](http://dhruvp.github.io/2015/03/07/re-frame/) by [Dhruv Parthasarathy] - A detailed step-by-step tutorial that ports the Angular Phonecat tutorial to re-frame. Based on re-frame `0.2.0`
|
||||
|
||||
* [Stately: State Machines](https://github.com/nodename/stately) also https://www.youtube.com/watch?v=klqorRUPluw
|
||||
|
||||
### Videos
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Question](#question)
|
||||
- [Answer](#answer)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
|
||||
### Question
|
||||
|
||||
How can I detect exceptions in Event Handlers?
|
||||
|
||||
### Answer
|
||||
|
||||
A suggested solution can be found in [this issue](https://github.com/Day8/re-frame/issues/231#issuecomment-249991378).
|
||||
|
||||
***
|
||||
|
||||
Up: [FAQ Index](README.md)
|
|
@ -1,3 +1,14 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Question](#question)
|
||||
- [Short Answer](#short-answer)
|
||||
- [Better Answer](#better-answer)
|
||||
- [Other Inspection Tools](#other-inspection-tools)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
|
||||
### Question
|
||||
|
||||
|
@ -61,5 +72,6 @@ There's also [Data Frisk](https://github.com/Odinodin/data-frisk-reagent) which
|
|||
provides a very nice solution for navigating and inspecting any data structure.
|
||||
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
Up: [FAQ Index](README.md)
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Question](#question)
|
||||
- [Answer](#answer)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
### Question
|
||||
|
||||
I use logging method X, how can I make re-frame use my method?
|
||||
|
@ -23,5 +32,6 @@ override that by providing your own set or subset of these functions using
|
|||
...})
|
||||
```
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
Up: [FAQ Index](README.md)
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Question](#question)
|
||||
- [Answer](#answer)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
### Question
|
||||
|
||||
If I `dispatch` a js event object (from a view), it is nullified
|
||||
|
@ -17,5 +26,6 @@ So there's two things to say about this:
|
|||
and debugging will become easier.
|
||||
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
Up: [FAQ Index](README.md)
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Frequently Asked Questions](#frequently-asked-questions)
|
||||
- [Want To Add An FAQ?](#want-to-add-an-faq)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
1. [How can I Inspect app-db?](Inspecting-app-db.md)
|
||||
2. [How do I use logging method X](Logging.md)
|
||||
3. [Dispatched Events Are Null](Null-Dispatched-Events.md)
|
||||
4.
|
||||
6. [Why implement re-frame in `.cljc` files](Why-CLJC.md)
|
||||
4. [Why implement re-frame in `.cljc` files](Why-CLJC.md)
|
||||
5. [Why do we need to clear the subscription cache when reloading with Figwheel?](Why-Clear-Sub-Cache.md)
|
||||
6. [How can I detect exceptions in Event Handlers?](CatchingEventExceptions.md)
|
||||
|
||||
|
||||
|
||||
|
||||
### Want To Add An FAQ?
|
||||
## Want To Add An FAQ?
|
||||
|
||||
We'd like that. Please supply a PR. Or just open an issue. Many Thanks!!
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Question](#question)
|
||||
- [Answer](#answer)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
### Question
|
||||
|
||||
Why is re-frame implemented in `.cljc` files? Aren't ClojureScript
|
||||
|
@ -16,5 +25,6 @@ Necessary interop for each platform can be found in
|
|||
See also: https://github.com/Day8/re-frame-test
|
||||
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
Up: [FAQ Index](README.md)
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Question](#question)
|
||||
- [Answer](#answer)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
### Question
|
||||
|
||||
Why do we call `clear-subscription-cache!` when reloading code with Figwheel?
|
||||
|
||||
### Answer
|
||||
|
||||
Pour yourself a drink, as this is a circuitous tale involving one of the hardest
|
||||
problems in Computer Science.
|
||||
|
||||
**1: Humble beginnings**
|
||||
|
||||
When React is rendering, if an exception is thrown, it doesn't catch or
|
||||
handle the errors gracefully. Instead, all of the React components up to
|
||||
the root are destroyed. When these components are destroyed, none of their
|
||||
standard lifecycle methods are called, like `ComponentDidUnmount`.
|
||||
|
||||
|
||||
**2: Simple assumptions**
|
||||
|
||||
Reagent tracks the watchers of a Reaction to know when no-one is watching and
|
||||
it can call the Reaction's `on-dispose`. Part of the book-keeping involved in
|
||||
this requires running the `on-dispose` in a React `ComponentWillUnmount` lifecycle
|
||||
method.
|
||||
|
||||
At this point, your spidey senses are probably tingling.
|
||||
|
||||
**3: The hardest problem in CS**
|
||||
|
||||
re-frame subscriptions are created as Reactions. re-frame helpfully deduplicates
|
||||
subscriptions if multiple parts of the view request the same subscription. This
|
||||
is a big efficiency boost. When re-frame creates the subscription Reaction, it
|
||||
sets the `on-dispose` method of that subscription to remove itself from the
|
||||
subscription cache. This means that when that subscription isn't being watched
|
||||
by any part of the view, it can be disposed.
|
||||
|
||||
**4: The gnarly implications**
|
||||
|
||||
If you are
|
||||
|
||||
1. Writing a re-frame app
|
||||
2. Write a bug in your subscription code (your one bug for the year)
|
||||
3. Which causes an exception to be thrown in your rendering code
|
||||
|
||||
then:
|
||||
|
||||
1. React will destroy all of the components in your view without calling `ComponentWillUnmount`.
|
||||
2. Reagent will not get notified that some subscriptions are not needed anymore.
|
||||
3. The subscription on-dispose functions that should have been run, are not.
|
||||
4. re-frame's subscription cache will not be invalidated correctly, and the subscription with the bug
|
||||
is still in the cache.
|
||||
|
||||
At this point you are looking at a blank screen. After debugging, you find the problem and fix it.
|
||||
You save your code and Figwheel recompiles and reloads the changed code. Figwheel attempts to re-render
|
||||
from the root. This causes all of the Reagent views to be rendered and to request re-frame subscriptions
|
||||
if they need them. Because the old buggy subscription is still sitting around in the cache, re-frame
|
||||
will return that subscription instead of creating a new one based on the fixed code. The only way around
|
||||
this (once you realise what is going on) is to reload the page.
|
||||
|
||||
**5: Coda**
|
||||
|
||||
re-frame 0.9.0 provides a new function: `re-frame.core/clear-subscription-cache!` which will run the
|
||||
on-dispose function for every subscription in the cache, emptying the cache, and causing new subscriptions
|
||||
to be created after reloading.
|
||||
|
||||
***
|
||||
|
||||
Up: [FAQ Index](README.md)
|
|
@ -1,26 +1,33 @@
|
|||
## Introduction
|
||||
|
||||
This is an interceptors tutorial.
|
||||
## re-frame Interceptors
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [Interceptors](#interceptors)
|
||||
* [Why Interceptors?](#why-interceptors-)
|
||||
* [What Do Interceptors Do?](#what-do-interceptors-do-)
|
||||
* [Wait, I know That Pattern!](#wait--i-know-that-pattern-)
|
||||
* [What's In The Pipeline?](#what-s-in-the-pipeline-)
|
||||
* [Show Me](#show-me)
|
||||
* [Handlers Are Interceptors Too](#handlers-are-interceptors-too)
|
||||
- [Why Interceptors?](#why-interceptors)
|
||||
- [What Do Interceptors Do?](#what-do-interceptors-do)
|
||||
- [Wait, I know That Pattern!](#wait-i-know-that-pattern)
|
||||
- [What's In The Pipeline?](#whats-in-the-pipeline)
|
||||
- [Show Me](#show-me)
|
||||
- [Handlers Are Interceptors Too](#handlers-are-interceptors-too)
|
||||
- [Executing A Chain](#executing-a-chain)
|
||||
* [The Links Of The Chain](#the-links-of-the-chain)
|
||||
* [What Is Context?](#what-is-context-)
|
||||
* [Self Modifying](#self-modifying)
|
||||
* [Credit](#credit)
|
||||
* [Write An Interceptor](#write-an-interceptor)
|
||||
* [Wrapping Handlers](#wrapping-handlers)
|
||||
- [The Links Of The Chain](#the-links-of-the-chain)
|
||||
- [What Is Context?](#what-is-context)
|
||||
- [Self Modifying](#self-modifying)
|
||||
- [Credit](#credit)
|
||||
- [Write An Interceptor](#write-an-interceptor)
|
||||
- [Wrapping Handlers](#wrapping-handlers)
|
||||
- [Summary](#summary)
|
||||
- [Appendix](#appendix)
|
||||
* [The Builtin Interceptors](#the-builtin-interceptors)
|
||||
- [The Builtin Interceptors](#the-builtin-interceptors)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Introduction
|
||||
|
||||
This is a tutorial on re-frame Interceptors.
|
||||
|
||||
## Interceptors
|
||||
### Why Interceptors?
|
||||
|
@ -89,8 +96,7 @@ concept, right there.
|
|||
|
||||
### Show Me
|
||||
|
||||
At the time when you register an event handler, you can provide an
|
||||
chain of interceptors too.
|
||||
At the time when you register an event handler, you can provide a chain of interceptors too.
|
||||
|
||||
Using a 3-arity registration function:
|
||||
```clj
|
||||
|
@ -105,24 +111,24 @@ Using a 3-arity registration function:
|
|||
|
||||
### Handlers Are Interceptors Too
|
||||
|
||||
You might see that registration above as associating `:some-id` with two things: (1) a chain of interceptors
|
||||
You might see that registration above as associating `:some-id` with two things: (1) a chain of 2 interceptors `[in1 in2]`
|
||||
and (2) a handler.
|
||||
|
||||
Except, the handler is turned into an interceptor too. (We'll see how shortly)
|
||||
Except, the handler is turned into an interceptor too (we'll see how shortly).
|
||||
|
||||
So `:some-id` is only associated with one thing: a 3-chain of interceptors,
|
||||
with the handler wrapped in an interceptor and put on the end of the other two.
|
||||
with the handler wrapped in an interceptor, called say `h`, and put on the end of the other two: `[in1 in2 h]`.
|
||||
|
||||
Except, the registration function itself, `reg-event-db`, actually takes this 3-chain
|
||||
and inserts its own interceptors
|
||||
(which do useful things) at the front (more on this soon too),
|
||||
so ACTUALLY, there's about 5 interceptors in the chain.
|
||||
and inserts its own standard interceptors, called say `std1` and `std2`
|
||||
(which do useful things, more soon) at the front,
|
||||
so ACTUALLY, there's about 5 interceptors in the chain: `[std1 std2 in1 in2 h]`
|
||||
|
||||
So, ultimately, that event registration associates the event id `:some-id`
|
||||
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.
|
||||
interceptors will be "executed". And that's how an event gets handled.
|
||||
|
||||
|
||||
## Executing A Chain
|
||||
|
@ -136,8 +142,13 @@ Each interceptor has this form:
|
|||
:after (fn [context] ...)} ;; `identity` would be a noop
|
||||
```
|
||||
|
||||
That's essentially a map of two functions. Now imagine a vector of these maps - that's an
|
||||
an interceptor chain.
|
||||
That's essentially a map of two functions. Now imagine a vector of these maps - that's an interceptor chain.
|
||||
|
||||
Above we imagined an interceptor chain of `[std1 std2 in1 in2 h]`. Now we know that this is really
|
||||
a vector of 5 maps: `[{...} {...} {...} {...} {...}]` where each of the 5 maps have
|
||||
a `:before` and `:after` fn.
|
||||
|
||||
Sometimes, the `:before` and `:after` fns are noops (think `identity`).
|
||||
|
||||
To "execute" an interceptor chain:
|
||||
1. create a `context` (a map, described below)
|
||||
|
@ -207,6 +218,9 @@ and removed from the `:queue` by existing Interceptors.
|
|||
|
||||
> All truths are easy to understand once they are discovered <br>
|
||||
> -- Galileo Galilei
|
||||
<br>
|
||||
> Things always become obvious after the fact <br>
|
||||
> -- Nassim Nicholas Taleb
|
||||
|
||||
This elegant and flexible arrangement was originally
|
||||
designed by the talented
|
||||
|
@ -216,7 +230,7 @@ designed by the talented
|
|||
|
||||
Dunno about you, but I'm easily offended by underscores.
|
||||
|
||||
If we had a component which did this:
|
||||
If we had a view which did this:
|
||||
```clj
|
||||
(dispatch [:delete-item 42])
|
||||
```
|
||||
|
@ -226,16 +240,16 @@ We'd have to write this handler:
|
|||
(reg-event-db
|
||||
:delete-item
|
||||
(fn
|
||||
[db [_ key-to-delete]] ;; <---- Arrgggghhh underscore
|
||||
[db [_ key-to-delete]] ;; <---- Arrgggghhh underscore
|
||||
(dissoc db key-to-delete)))
|
||||
```
|
||||
|
||||
Do you see it there? In the event destructuring!!! Almost mocking us with that
|
||||
passive aggressive, understated thing it has going on!! Co-workers
|
||||
have said I'm "being overly sensitive", perhaps even horizontalist, but
|
||||
you can see it, right? Of course you can.
|
||||
have said I'm "being overly sensitive", perhaps even pixel-ist, but
|
||||
you can see it, right? Of course you can.
|
||||
|
||||
What a relief it would be to get rid of it, but how? We'll write an interceptor: `trim-event`
|
||||
What a relief it would be to not have it there, but how? We'll write an interceptor: `trim-event`
|
||||
|
||||
Once we have written `trim-event`, our registration will change to look like this:
|
||||
```clj
|
||||
|
@ -243,11 +257,11 @@ Once we have written `trim-event`, our registration will change to look like thi
|
|||
:delete-item
|
||||
[trim-event] ;; <--- interceptor added
|
||||
(fn
|
||||
[db [key-to-delete]] ;; <--- no leading underscore necessary
|
||||
[db [key-to-delete]] ;; <---yaaah! no leading underscore
|
||||
(dissoc db key-to-delete)))
|
||||
```
|
||||
|
||||
`trim-event` will need to change the `:coeffects` map (within `context`). More specifically, it will be
|
||||
`trim-event` will need to change the `:coeffects` map (within `context`). Specifically, it will be
|
||||
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
|
||||
|
@ -279,16 +293,20 @@ Notes:
|
|||
We're going well. Let's do an advanced wrapping.
|
||||
|
||||
Earlier, in the "Handlers Are Interceptors Too" section, I explained that `event handlers`
|
||||
are wrapped in an Interceptor and placed on the end of an Interceptor chain.
|
||||
How does this wrapping happen?
|
||||
are wrapped in an Interceptor and placed on the end of an Interceptor chain. Remember the
|
||||
whole `[std1 std2 in1 in2 h]` thing?
|
||||
|
||||
We're now look at the `h` bit. How does an event handler get wrapped to be an Interceptor.
|
||||
|
||||
Reminder - there's two kinds of handler:
|
||||
- the `-db` variety registered by `reg-event-db`
|
||||
- the `-fx` variety registered by `reg-event-fx`
|
||||
|
||||
I'll now show how to wrap the `-db` variety. Here's what a `-db` handler looks like:
|
||||
I'll now show how to wrap the `-db` variety.
|
||||
|
||||
Reminder: here's what a `-db` handler looks like:
|
||||
```clj
|
||||
(fn [db event] ;; takes two params
|
||||
(fn [db event] ;; takes two params
|
||||
(assoc db :flag true)) ;; returns a new db
|
||||
```
|
||||
|
||||
|
@ -306,11 +324,11 @@ Interceptor which wraps that handler:
|
|||
```
|
||||
|
||||
Notes:
|
||||
1. Notice how this wrapper extracts data from the `context's` `:coeffect`
|
||||
1. Notice how this wrapper extracts data from the `context's` `:coeffects`
|
||||
and then calls the handler with that data (a handler must be called with `db` and `event`)
|
||||
2. Equally notice how this wrapping takes the return value from the `-db`
|
||||
handler and puts it into `context's` `:effect`
|
||||
3. The modified `context` (it has a new `:effect`) is returned
|
||||
handler and puts it into `context's` `:effects`
|
||||
3. The modified `context` (it has a new `:effects`) is returned
|
||||
3. This is all done in `:before`. There is no `:after` (it is a noop). But this
|
||||
could have been reversed with the work happening in `:after` and `:before` a noop. Shrug.
|
||||
Remember that this Interceptor will be on the end of a chain.
|
||||
|
@ -331,7 +349,7 @@ __1.__ When you register an event handler, you can supply a collection of interc
|
|||
```
|
||||
|
||||
__2.__ When you are registering an event handler, you are associating an event id with a chain of interceptors including:
|
||||
- the ones your supply (optional)
|
||||
- the ones you supply (optional)
|
||||
- an extra one on the end, which wraps the handler itself
|
||||
- a couple at the beginning of the chain, put there by the `reg-event-db` or `reg-event-fx`.
|
||||
|
||||
|
@ -347,12 +365,6 @@ __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: [Effectful Handlers](EffectfulHandlers.md)
|
||||
Up: [Index](README.md)
|
||||
Next: [Effects](Effects.md)
|
||||
|
||||
|
||||
## Appendix
|
||||
|
||||
### The Builtin Interceptors
|
||||
|
@ -377,7 +389,8 @@ To use them, first require them:
|
|||
[re-frame.core :refer [debug path]])
|
||||
```
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
Previous: [Effectful Handlers](EffectfulHandlers.md)
|
||||
Up: [Index](README.md)
|
||||
Next: [Effects](Effects.md)
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Bootstrapping Application State](#bootstrapping-application-state)
|
||||
- [1. Register Handlers](#1-register-handlers)
|
||||
- [2. Kick Start Reagent](#2-kick-start-reagent)
|
||||
- [3. Loading Initial Data](#3-loading-initial-data)
|
||||
- [Getting Data Into `app-db`](#getting-data-into-app-db)
|
||||
- [The Pattern](#the-pattern)
|
||||
- [Scales Up](#scales-up)
|
||||
- [Cheating - Synchronous Dispatch](#cheating---synchronous-dispatch)
|
||||
- [Loading Initial Data From Services](#loading-initial-data-from-services)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Bootstrapping Application State
|
||||
|
||||
To bootstrap a re-frame application, you need to:
|
||||
|
@ -17,8 +33,8 @@ Point 3 is the interesting bit and will be the main focus of this page, but let'
|
|||
|
||||
## 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:
|
||||
re-frame's various handlers all work in the same way. You declare
|
||||
and register 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
|
||||
|
@ -65,8 +81,7 @@ And now we use that subscription:
|
|||
(defn main-panel
|
||||
[]
|
||||
(let [name (re-frame/subscribe [:name])] ;; <--- a subscription <---
|
||||
(fn []
|
||||
[:div "Hello " @name])))) ;; <--- use the result of the subscription
|
||||
[:div "Hello " @name]))) ;; <--- use the result of the subscription
|
||||
```
|
||||
|
||||
The user of our app will see funny things
|
||||
|
@ -142,16 +157,14 @@ quick sketch of the entire pattern. It is very straight-forward.
|
|||
(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]))))
|
||||
[: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
|
||||
(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
|
||||
[]
|
||||
|
@ -193,11 +206,10 @@ This assumes boolean flags are set in `app-db` when data was loaded from these s
|
|||
|
||||
## 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.
|
||||
In simple cases, you can simplify matters by using `dispatch-sync` (instead of `dispatch`) in
|
||||
the main function.
|
||||
|
||||
This technique can be seen in the [TodoMVC Example](https://github.com/Day8/re-frame/blob/master/examples/todomvc/src/todomvc/core.cljs#L35).
|
||||
|
||||
`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
|
||||
|
@ -214,11 +226,12 @@ tool in this context and, sometimes, when writing tests, but
|
|||
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.
|
||||
The next Tutorial will show you how.
|
||||
|
||||
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
Previous: [Namespaced Keywords](Namespaced-Keywords.md)
|
||||
Up: [Index](README.md)
|
||||
Next: [Talking To Servers](Talking-To-Servers.md)
|
||||
|
|
|
@ -0,0 +1,283 @@
|
|||
|
||||
## Mental Model Omnibus
|
||||
|
||||
> If a factory is torn down but the rationality which produced it is
|
||||
left standing, then that rationality will simply produce another
|
||||
factory. If a revolution destroys a government, but the systematic
|
||||
patterns of thought that produced that government are left intact,
|
||||
then those patterns will repeat themselves. <br>
|
||||
> -- Robert Pirsig, Zen and the Art of Motorcycle Maintenance
|
||||
|
||||
|
||||
<img height="350px" align="right" src="/images/mental-model-omnibus.jpg?raw=true">
|
||||
|
||||
The re-frame tutorials initially focus on the **domino
|
||||
narrative**. The goal is to efficiently explain the mechanics of re-frame,
|
||||
and get you reading and writing code ASAP.
|
||||
|
||||
But **there are other perspectives** on re-frame
|
||||
which will deepen your understanding.
|
||||
|
||||
This tutorial is a tour of these ideas, justifications and insights.
|
||||
It is a little rambling, but I'm hoping it will deliver for you
|
||||
at least one "Aaaah, I see" moment before the end.
|
||||
|
||||
> All models are wrong, but some are useful
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [What is the problem?](#what-is-the-problem)
|
||||
- [Guiding Philosophy](#guiding-philosophy)
|
||||
- [It does Event Sourcing](#it-does-event-sourcing)
|
||||
- [It does a reduce](#it-does-a-reduce)
|
||||
- [Derived Data All The Way Down](#derived-data-all-the-way-down)
|
||||
- [It does FSM](#it-does-fsm)
|
||||
- [Full Stack](#full-stack)
|
||||
- [What Of This Romance?](#what-of-this-romance)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## What is the problem?
|
||||
|
||||
First, we decided to build our SPA apps with ClojureScript, then we
|
||||
choose [Reagent], then we had a problem. It was mid 2014.
|
||||
|
||||
For all its considerable brilliance, Reagent (+ React)
|
||||
delivers only the 'V' part of a traditional MVC framework.
|
||||
|
||||
But apps involve much more than V. We build quite complicated
|
||||
SPAs which can run to 50K lines of code. So, I wanted to know:
|
||||
where does the control logic go? How is state stored & manipulated? etc.
|
||||
|
||||
We read up on [Pedestal App], [Flux],
|
||||
[Hoplon], [Om], early [Elm], etc and re-frame is the architecture that
|
||||
emerged. Since then, we've tried to keep an eye on further developments like the
|
||||
Elm Architecture, Om.Next, BEST, Cycle.js, Redux, etc. They have taught us much
|
||||
although we have often made different choices.
|
||||
|
||||
re-frame does have parts which correspond to M, V, and C, but they aren't objects.
|
||||
It is sufficiently different in nature
|
||||
from (traditional, Smalltalk) MVC that calling it MVC would be confusing. I'd
|
||||
love an alternative.
|
||||
|
||||
Perhaps it is a RAVES framework - Reactive-Atom Views Event
|
||||
Subscription framework (I love the smell of acronym in the morning).
|
||||
|
||||
Or, if we distill to pure essence, `DDATWD` - Derived Data All The Way Down.
|
||||
|
||||
*TODO:* get acronym down to 3 chars! Get an image of stacked Turtles for `DDATWD`
|
||||
insider's joke, conference T-Shirt.
|
||||
|
||||
## Guiding Philosophy
|
||||
|
||||
__First__, above all we believe in the one true [Dan Holmsand], creator of Reagent, and
|
||||
his divine instrument the `ratom`. We genuflect towards Sweden once a day.
|
||||
|
||||
__Second__, we believe in ClojureScript, immutable data and the process of building
|
||||
a system out of pure functions.
|
||||
|
||||
__Third__, we believe in the primacy of data, for the reasons described in
|
||||
the main README. re-frame has a data oriented, functional architecture.
|
||||
|
||||
__Fourth__, we believe that Reactive Programming is one honking good idea.
|
||||
How did we ever live without it? It is a quite beautiful solution to one half of re-frame's
|
||||
data conveyance needs, **but** we're cautious about taking it too far - as far as, say, cycle.js.
|
||||
It doesn't take over everything in re-frame - it just does part of the job.
|
||||
|
||||
__Finally__, many years ago I programmed briefly in Eiffel where I learned
|
||||
about [command-query separation](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation).
|
||||
Each generation of
|
||||
programmers seems destined to rediscover this principle - CQRS is the recent re-rendering.
|
||||
And yet we still see read/write `cursors` and two way data binding being promoted as a good thing.
|
||||
Please, just say no. As your programs get bigger, the use of these two-way constructs
|
||||
will encourage control logic into all the
|
||||
wrong places and you'll end up with a tire fire of an Architecture.
|
||||
|
||||
|
||||
## It does Event Sourcing
|
||||
|
||||
|
||||
How did that error happen, you puzzle, shaking your head ruefully?
|
||||
What did the user do immediately prior? What
|
||||
state was the app in that this event was so problematic?
|
||||
|
||||
To debug, you need to know this information:
|
||||
1. the state of the app immediately before the exception
|
||||
2. What final `event` then caused your app to error
|
||||
|
||||
Well, with re-frame you need to record (have available):
|
||||
1. A recent checkpoint of the application state in `app-db` (perhaps the initial state)
|
||||
2. all the events `dispatch`ed since the last checkpoint, up to the point where the error occurred
|
||||
|
||||
Note: that's all just data. **Pure, lovely loggable data.**
|
||||
|
||||
If you have that data, then you can reproduce the error.
|
||||
|
||||
re-frame allows you to time travel, even in a production setting.
|
||||
Install the "checkpoint" state into `app-db`
|
||||
and then "play forward" through the collection dispatched events.
|
||||
|
||||
The only way the app "moves forwards" is via events. "Replaying events" moves you
|
||||
step by step towards the error causing problem.
|
||||
|
||||
This is perfect for debugging assuming, of course, you are in a position to capture
|
||||
a checkpoint of `app-db`, and the events since then.
|
||||
|
||||
Here's Martin Fowler's [description of Event Sourcing](http://martinfowler.com/eaaDev/EventSourcing.html).
|
||||
|
||||
## It does a reduce
|
||||
|
||||
|
||||
Here's an interesting way of thinking about the re-frame
|
||||
data flow ...
|
||||
|
||||
**First**, imagine that all the events ever dispatched in a
|
||||
certain running app were stored in a collection (yes, event sourcing again).
|
||||
So, if when the app started, the user clicked on button X
|
||||
the first item in this collection would be the event
|
||||
generated by that button, and then, if next the user moved
|
||||
a slider, the associated event would be the next item in
|
||||
the collection, and so on and so on. We'd end up with a
|
||||
collection of event vectors.
|
||||
|
||||
**Second**, remind yourself that the `combining function`
|
||||
of a `reduce` takes two arguments:
|
||||
1. the current state of the reduction and
|
||||
2. the next collection member to fold in
|
||||
|
||||
Then notice that `reg-event-db` event handlers take two arguments also:
|
||||
1. `db` - the current state of `app-db`
|
||||
2. `v` - the next event to fold in
|
||||
|
||||
Interesting. That's the same as a `combining function` in a `reduce`!!
|
||||
|
||||
So now we can introduce the new mental model: at any point in time,
|
||||
the value in `app-db` is the result of performing a `reduce` over
|
||||
the entire `collection` of events dispatched in the app up until
|
||||
that time. The combining function for this reduce is the set of event handlers.
|
||||
|
||||
It is almost like `app-db` is the temporary place where this
|
||||
imagined `perpetual reduce` stores its on-going reduction.
|
||||
|
||||
Now, in the general case, this perspective breaks down a bit,
|
||||
because of `reg-event-fx` (has `-fx` on the end, not `-db`) which
|
||||
allows:
|
||||
1. event handlers can produce `effects` beyond just application state
|
||||
changes.
|
||||
2. Event handlers sometimes need coeffects (arguments) in addition to `db` and `v`.
|
||||
|
||||
But, even if it isn't the full picture, it is a very useful
|
||||
and interesting mental model. We were first exposed to this idea
|
||||
via Elm's early use of `foldp` (fold from the past), which was later enshrined in the
|
||||
Elm Architecture.
|
||||
|
||||
## Derived Data All The Way Down
|
||||
|
||||
For the love of all that is good, please watch this terrific
|
||||
[StrangeLoop presentation](https://www.youtube.com/watch?v=fU9hR3kiOK0) (40 mins).
|
||||
See what happens when you re-imagine a database as a stream!! Look at
|
||||
all the problems that evaporate.
|
||||
Think about that: shared mutable state (the root of all evil),
|
||||
re-imagined as a stream!! Blew my socks off.
|
||||
|
||||
If, by chance, you ever watched that video (you should!), you might then twig to
|
||||
the idea that `app-db` is really a derived value ... the video talks
|
||||
a lot about derived values. So, yes, app-db is a derived value of the `perpetual reduce`.
|
||||
|
||||
And yet, it acts as the authoritative source of state in the app. And yet,
|
||||
it isn't, it is simply a piece of derived state. And yet, it is the source. Etc.
|
||||
|
||||
This is an infinite loop of sorts - an infinite loop of derived data.
|
||||
|
||||
## It does FSM
|
||||
|
||||
> Any sufficiently complicated GUI contains an ad hoc,
|
||||
> informally-specified, bug-ridden, slow implementation
|
||||
> of a hierarchical Finite State Machine <br>
|
||||
> -- me, trying too hard to impress my two twitter followers
|
||||
|
||||
`event handlers` collectively
|
||||
implement the "control" part of an application. Their logic
|
||||
interprets arriving events in the context of existing state,
|
||||
and they compute what the new state of the application.
|
||||
|
||||
`events` act, then, a bit like the `triggers` in a finite state machine, and
|
||||
the event handlers act like the rules which govern how the state machine
|
||||
moves from one logical state to the next.
|
||||
|
||||
In the simplest
|
||||
case, `app-db` will contain a single value which represents the current "logical state".
|
||||
For example, there might be a single `:phase` key which can have values like `:loading`,
|
||||
`:not-authenticated` `:waiting`, etc. Or, the "logical state" could be a function
|
||||
of many values in `app-db`.
|
||||
|
||||
Not every app has lots of logical states, but some do, and if you are implementing
|
||||
one of them, then formally recognising it and using a technique like
|
||||
[State Charts](https://www.amazon.com/Constructing-User-Interface-Statecharts-Horrocks/dp/0201342782)
|
||||
will help greatly in getting a clean design and fewer bugs.
|
||||
|
||||
The beauty of re-frame from a FSM point of view is that all the state is
|
||||
in one place - unlike OO systems where the state is distributed (and synchronized)
|
||||
across many objects. So implementing your control logic as a FSM is
|
||||
fairly natural in re-frame, whereas it is often difficult and
|
||||
contrived in other kinds of architecture (in my experience).
|
||||
|
||||
So, members of the jury, I put it to you that:
|
||||
- the first 3 dominoes implement an [Event-driven finite-state machine](https://en.wikipedia.org/wiki/Event-driven_finite-state_machine)
|
||||
- the last 3 dominoes render of the FSM's current state for the user to observe
|
||||
|
||||
Depending on your app, this may or may not be a useful mental model,
|
||||
but one thing is for sure ...
|
||||
|
||||
Events - that's the way we roll.
|
||||
|
||||
|
||||
|
||||
## Full Stack
|
||||
|
||||
If you like re-frame and want to take the principles full-stack, then
|
||||
these resource might be interesting to you:
|
||||
|
||||
Commander Pattern
|
||||
https://www.youtube.com/watch?v=B1-gS0oEtYc
|
||||
|
||||
Datalog All The Way Down
|
||||
https://www.youtube.com/watch?v=aI0zVzzoK_E
|
||||
|
||||
## What Of This Romance?
|
||||
|
||||
My job is to be a relentless cheerleader for re-frame, right?
|
||||
The gyrations of my Pom-Poms should be tectonic,
|
||||
but the following quote makes me smile. It should
|
||||
be taught in all ComSci courses.
|
||||
|
||||
> We begin in admiration and end by organizing our disappointment <br>
|
||||
> -- Gaston Bachelard (French philosopher)
|
||||
|
||||
Of course, that only applies if you get passionate about a technology
|
||||
(a flaw of mine).
|
||||
|
||||
But, no. No! Those French Philosophers and their pessimism - ignore him!!
|
||||
Your love for re-frame will be deep, abiding and enriching.
|
||||
|
||||
***
|
||||
|
||||
Previous: [First Code Walk-Through](CodeWalkthrough.md)
|
||||
Up: [Index](README.md)
|
||||
Next: [Effectful Handlers](EffectfulHandlers.md)
|
||||
|
||||
|
||||
[SPAs]:http://en.wikipedia.org/wiki/Single-page_application
|
||||
[SPA]:http://en.wikipedia.org/wiki/Single-page_application
|
||||
[Reagent]:http://reagent-project.github.io/
|
||||
[Dan Holmsand]:https://twitter.com/holmsand
|
||||
[Flux]:http://facebook.github.io/flux/docs/overview.html#content
|
||||
[Hiccup]:https://github.com/weavejester/hiccup
|
||||
[FRP]:https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
|
||||
[Elm]:http://elm-lang.org/
|
||||
[OM]:https://github.com/swannodette/om
|
||||
[Prismatic Schema]:https://github.com/Prismatic/schema
|
||||
[Hoplon]:http://hoplon.io/
|
||||
[Pedestal App]:https://github.com/pedestal/pedestal-app
|
|
@ -1,3 +1,11 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Namespaced Ids](#namespaced-ids)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Namespaced Ids
|
||||
|
||||
As an app gets bigger, you'll tend to get clashes on ids - event-ids, or query-ids (subscriptions), etc.
|
||||
|
@ -20,7 +28,8 @@ fiction. I can have the keyword `:panel1/edit` even though
|
|||
Naturally, you'll take advantage of this by using keyword namespaces
|
||||
which are both unique and descriptive.
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
Previous: [Navigation](Navigation.md)
|
||||
Up: [Index](README.md)
|
||||
Next: [Loading Initial Data](Loading-Initial-Data.md)
|
||||
Next: [Loading Initial Data](Loading-Initial-Data.md)
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [What About Navigation?](#what-about-navigation)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
|
||||
## What About Navigation?
|
||||
|
||||
|
@ -49,7 +57,8 @@ A high level reagent view has a subscription to :active-panel and will switch to
|
|||
|
||||
Continue to [Namespaced Keywords](Namespaced-Keywords.md) to reduce clashes on ids.
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
Previous: [Basic App Structure](Basic-App-Structure.md)
|
||||
Up: [Index](README.md)
|
||||
Next: [Namespaced Keywords](Namespaced-Keywords.md)
|
||||
|
|
|
@ -1,12 +1,26 @@
|
|||
## Eek! Performance Problems
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [1. Is It The `debug` Interceptor?](#1-is-it-the-debug-interceptor)
|
||||
- [2. `=` On Big Structures](#2--on-big-structures)
|
||||
- [An Example Of Problem 2](#an-example-of-problem-2)
|
||||
- [Solutions To Problem 2](#solutions-to-problem-2)
|
||||
- [3. Are you Using a React `key`?](#3-are-you-using-a-react-key)
|
||||
- [4. Callback Functions](#4-callback-functions)
|
||||
- [A Weapon](#a-weapon)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## 1. Is It The `debug` Interceptor?
|
||||
|
||||
This first one is something of a non-problem.
|
||||
|
||||
Are you are using the `re-frame.core/debug` Interceptor?
|
||||
You should be, it's useful. __But__ you do need to be aware of its possible performance implications.
|
||||
|
||||
|
||||
`debug` reports what's changed after an event handler has run by using
|
||||
`clojure.data/diff` to do deep, CPU intensive diff on `app-db`.
|
||||
That diff could be taking a while, and leading to apparent performance problems.
|
||||
|
@ -119,7 +133,7 @@ Look at this `div`:
|
|||
```
|
||||
|
||||
Every time it is rendered, that `:on-mouse-over` function will be regenerated,
|
||||
and it will NOT test equal to the last time it rendered. It will appear to be a new function.
|
||||
and it will NOT test `=` to the last time it rendered. It will appear to be a new function.
|
||||
It will appear to React that it has to replace the event handler.
|
||||
|
||||
Most of the time, this is not an issue. But if you are generating a LOT of DOM
|
||||
|
|
|
@ -1,28 +1,37 @@
|
|||
### Understanding Event Handlers
|
||||
- [-db Event Handlers] TODO
|
||||
- [Effectful Handlers](EffectfulHandlers.md)
|
||||
- [Interceptors](Interceptors.md)
|
||||
- [Effects](Effects.md)
|
||||
- [Coeffects](Coeffects.md)
|
||||
### Introduction
|
||||
|
||||
- [This Repo's README](../README.md)
|
||||
- [app-db (Application State)](ApplicationState.md)
|
||||
- [First Code Walk-Through](CodeWalkthrough.md)
|
||||
- [Mental Model Omnibus](MentalModelOmnibus.md)
|
||||
|
||||
|
||||
### Structuring Your Application
|
||||
### Event Handlers
|
||||
|
||||
- [Infographic Overview](EventHandlingInfographic.md)
|
||||
- [Effectful Handlers](EffectfulHandlers.md)
|
||||
- [Interceptors](Interceptors.md)
|
||||
- [Effects](Effects.md)
|
||||
- [Coeffects](Coeffects.md)
|
||||
|
||||
|
||||
### App Structure
|
||||
|
||||
- [Basic App Structure](Basic-App-Structure.md)
|
||||
- [Navigation](Navigation.md)
|
||||
- [Namespaced Keywords](Namespaced-Keywords.md)
|
||||
|
||||
|
||||
### Populating Your Application Data
|
||||
### App Data
|
||||
|
||||
- [Loading Initial Data](Loading-Initial-Data.md)
|
||||
- [Talking To Servers](Talking-To-Servers.md)
|
||||
- [Subscribing to External Data](Subscribing-To-External-Data.md)
|
||||
|
||||
|
||||
### Debugging And Testing
|
||||
### Debugging And Testing
|
||||
|
||||
- [Debugging-Event-Handlers](Debugging-Event-Handlers.md)
|
||||
- [Debugging Event Handlers](Debugging-Event-Handlers.md)
|
||||
- [Debugging](Debugging.md)
|
||||
|
||||
|
||||
|
@ -33,3 +42,8 @@
|
|||
- [Solve the CPU hog problem](Solve-the-CPU-hog-problem.md)
|
||||
- [Using Stateful JS Components](Using-Stateful-JS-Components.md)
|
||||
- [The re-frame Logo](The-re-frame-logo.md)
|
||||
|
||||
<!-- We put these at the end so that there is nothing for doctoc to generate. -->
|
||||
<!-- START doctoc -->
|
||||
<!-- END doctoc -->
|
||||
|
||||
|
|
|
@ -1,3 +1,18 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Solving The CPU Hog Problem](#solving-the-cpu-hog-problem)
|
||||
- [The re-frame Solution](#the-re-frame-solution)
|
||||
- [A Sketch](#a-sketch)
|
||||
- [Why Does A Redispatch Work?](#why-does-a-redispatch-work)
|
||||
- [Variations](#variations)
|
||||
- [Cancel Button](#cancel-button)
|
||||
- [Further Notes](#further-notes)
|
||||
- [Forcing A One Off Render](#forcing-a-one-off-render)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Solving The CPU Hog Problem
|
||||
|
||||
Sometimes a handler has a lot of CPU intensive work to do, and
|
||||
|
@ -57,7 +72,7 @@ Here's an `-fx` handler which counts up to some number in chunks:
|
|||
;; We are at the beginning, so:
|
||||
;; - modify db, causing popup of Modal saying "Working ..."
|
||||
;; - begin iterative dispatch. Give initial version of "so-far"
|
||||
{:disptch 1[:count-to false {:counter 0} finish-at] ;; dispatch to self
|
||||
{:dispatch [:count-to false {:counter 0} finish-at] ;; dispatch to self
|
||||
:db (assoc db :we-are-working true)}
|
||||
(if (> (:counter so-far) finish-at)
|
||||
;; We are finished:
|
||||
|
|
|
@ -1,3 +1,23 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Subscribing to External Data](#subscribing-to-external-data)
|
||||
- [There Can Be Only One!!](#there-can-be-only-one)
|
||||
- [Components Don't Know, Don't Care](#components-dont-know-dont-care)
|
||||
- [A 2nd Source](#a-2nd-source)
|
||||
- [Via A Subscription](#via-a-subscription)
|
||||
- [The Subscription Handler's Job](#the-subscription-handlers-job)
|
||||
- [Some Code](#some-code)
|
||||
- [Any Good?](#any-good)
|
||||
- [Warning: Undo/Redo](#warning-undoredo)
|
||||
- [Query De-duplication](#query-de-duplication)
|
||||
- [Thanks To](#thanks-to)
|
||||
- [The Alternative Approach](#the-alternative--approach)
|
||||
- [What Not To Do](#what-not-to-do)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Subscribing to External Data
|
||||
|
||||
In [Talking To Servers](Talking-To-Servers.md) we learned how to
|
||||
|
@ -236,6 +256,7 @@ data into HTML and nothing more. they absolutely do not do imperative stuff.
|
|||
|
||||
Use one of the two alternatives described above.
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
Previous: [Talking to Servers](Talking-To-Servers.md)
|
||||
Up: [Index](README.md)
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Talking To Servers](#talking-to-servers)
|
||||
- [Triggering The Request](#triggering-the-request)
|
||||
- [The Event Handler](#the-event-handler)
|
||||
- [Version 1](#version-1)
|
||||
- [Successful GET](#successful-get)
|
||||
- [Problems In Paradise?](#problems-in-paradise)
|
||||
- [Version 2](#version-2)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Talking To Servers
|
||||
|
||||
This page describes how a re-frame app might "talk" to a backend HTTP server.
|
||||
|
@ -117,6 +131,7 @@ Here's our rewrite:
|
|||
```clj
|
||||
(ns my.app.events
|
||||
(:require
|
||||
[ajax.core :as ajax]
|
||||
[day8.re-frame.http-fx]
|
||||
[re-frame.core :refer [reg-event-fx]))
|
||||
|
||||
|
@ -128,6 +143,7 @@ Here's our rewrite:
|
|||
;; we return a map of (side) effects
|
||||
{:http-xhrio {:method :get
|
||||
:uri "http://json.my-endpoint.com/blah"
|
||||
:response-format (ajax/json-response-format {:keywords? true})
|
||||
:on-success [:process-response]
|
||||
:on-failure [:bad-response]}
|
||||
:db (assoc db :loading? true)}))
|
||||
|
@ -139,7 +155,8 @@ Notes:
|
|||
|
||||
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
Previous: [Loading Initial Data](Loading-Initial-Data.md)
|
||||
Up: [Index](README.md)
|
||||
Next: [Subscribing to External Data](Subscribing-To-External-Data.md)
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Testing](#testing)
|
||||
- [Event Handlers - Part 1](#event-handlers---part-1)
|
||||
- [Event Handlers - Part 2](#event-handlers---part-2)
|
||||
- [Subscription Handlers](#subscription-handlers)
|
||||
- [Components- Part 1](#components--part-1)
|
||||
- [Components - Part 2A](#components---part-2a)
|
||||
- [Components - Part 2B](#components---part-2b)
|
||||
- [Components - Part 2C](#components---part-2c)
|
||||
- [Summary](#summary)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
> IGNORE THIS DOCUMENT. IT IS WIP
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
With a re-frame app, there's principally three things to test:
|
||||
1. Event handlers
|
||||
2. Subscription handlers
|
||||
3. View functions
|
||||
|
||||
## Event Handlers - Part 1
|
||||
|
||||
Event Handlers are pure functions and consequently easy to test.
|
||||
|
||||
First, create an event handler like this:
|
||||
```clj
|
||||
(defn my-db-handler
|
||||
[db v]
|
||||
... return a modified version of db)
|
||||
```
|
||||
|
||||
Then, register it in a separate step:
|
||||
```clj
|
||||
(re-event-db
|
||||
:some-id
|
||||
[some-interceptors]
|
||||
my-handler)
|
||||
```
|
||||
|
||||
With this setup, `my-db-handler` is available for direct testing.
|
||||
|
||||
Your unittests will pass in certain values for `db` and `v`, and then ensure it returns the right (modified version of) `db`.
|
||||
|
||||
## Event Handlers - Part 2
|
||||
|
||||
Event handlers mutate the value in `app-db` - that's their job.
|
||||
|
||||
I'd recommend defining a [Prismatic Schema](https://github.com/Prismatic/schema)
|
||||
for the value in `app-db` and then checking for correctness after every,
|
||||
single event handler. Every single one.
|
||||
|
||||
Using `after` middleware, this is easy to arrange. The todomvc example shows how:
|
||||
- [define a Schema](https://github.com/Day8/re-frame/blob/2ba8914d8dd5f0cf2b09d6f3942823a798c2ef5c/examples/todomvc/src/todomvc/db.cljs#L6-L28) for the value in `app-db`
|
||||
- [create some middleware](https://github.com/Day8/re-frame/blob/2ba8914d8dd5f0cf2b09d6f3942823a798c2ef5c/examples/todomvc/src/todomvc/handlers.cljs#L11-L19)
|
||||
- [add the middleware](https://github.com/Day8/re-frame/blob/2ba8914d8dd5f0cf2b09d6f3942823a798c2ef5c/examples/todomvc/src/todomvc/handlers.cljs#L46) to your event handlers
|
||||
|
||||
## Subscription Handlers
|
||||
|
||||
Here's a subscription handler from [the todomvc example](https://github.com/Day8/re-frame/blob/master/examples/todomvc/src/todomvc/subs.cljs):
|
||||
|
||||
```clj
|
||||
(reg-sub
|
||||
:completed-count
|
||||
(fn [db _]
|
||||
(completed-count (:todos db))))
|
||||
```
|
||||
|
||||
How do we test this?
|
||||
|
||||
We could split the handler function from its registration, like this:
|
||||
```clj
|
||||
(defn get-completed-count
|
||||
[app-db _]
|
||||
(reaction (completed-count (:todos @app-db))))
|
||||
|
||||
(register-sub
|
||||
:completed-count
|
||||
get-completed-count)
|
||||
```
|
||||
|
||||
That makes `get-completed-count` available for direct testing. But you'll note it isn't a pure function.
|
||||
It isn't values in, values out. Instead, it is atoms in, atoms out.
|
||||
|
||||
If this function was on a paint chart, they'd call in "Arctic Fusion" to indicate its
|
||||
proximity to pure white, while hinting at taints.
|
||||
|
||||
We could accept this. We could create tests by passing in a `reagent/atom` holding the
|
||||
certain values and then checking the values in what's returned. That would work.
|
||||
The more pragmatic among us might even approve.
|
||||
|
||||
But let's assume that's not good enough. Let's refactor for pureness:
|
||||
|
||||
The 1st step in this refactor is to create a pure function which actually does ALL the hard work ...
|
||||
```clj
|
||||
(defn completed-count-handler
|
||||
[db v] ;; db is a value, not an atom
|
||||
..... return a value here based on the values db and v)
|
||||
```
|
||||
|
||||
The 2nd step in the refactor is to register using a thin `reaction` wrapper:
|
||||
```clj
|
||||
(register-sub
|
||||
:completed-count
|
||||
(fn [app-db v]
|
||||
(reaction (completed-count-handler @app-db v))))
|
||||
```
|
||||
|
||||
Because `completed-count-handler` is now doing all the work, it is the thing we want
|
||||
to test, and it is now a pure function. So I think we are there.
|
||||
|
||||
## Components- Part 1
|
||||
|
||||
Components/views are slightly more tricky. There's a few options.
|
||||
|
||||
First, I have to admit an ugly secret. I don't tend to write tests for my views.
|
||||
Hey, don't give me that disproving frown! I have my reasons.
|
||||
|
||||
Remember that every line of code you write is a liability. So tests have to earn
|
||||
their keep - they have to deliver a good cost / benefit ratio. And, in my experience
|
||||
with the re-frame architecture, the Reagent view components tend to be an unlikely
|
||||
source of bugs. There's just not much logic in them for me to get wrong.
|
||||
|
||||
Okay, fine, don't believe me, then!!
|
||||
|
||||
Here's how, theoretically, I'd write tests if I wasn't me ...
|
||||
|
||||
If a Components is a [Form-1](https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components#form-1-a-simple-function) structure, then it is fairly easy to test.
|
||||
|
||||
A trivial example:
|
||||
```clj
|
||||
(defn greet
|
||||
[name]
|
||||
[:div "Hello " name])
|
||||
|
||||
(greet "Wiki")
|
||||
;;=> [:div "Hello " "Wiki"]
|
||||
```
|
||||
|
||||
So, here, testing involves passing values into the function and checking the data structure returned for correctness.
|
||||
|
||||
What's returned is hiccup, of course. So how do you test hiccup for correctness?
|
||||
|
||||
hiccup is just a clojure data structure - vectors containing keywords, and maps, and other vectors, etc. Perhaps you'd use https://github.com/nathanmarz/specter to declaratively check on the presence of certain values and structures? Or do it more manually.
|
||||
|
||||
|
||||
## Components - Part 2A
|
||||
|
||||
But what if the Component has a subscription (via a [Form-2](https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components#form-2--a-function-returning-a-function) structure)?
|
||||
|
||||
```clj
|
||||
(defn my-view
|
||||
[something]
|
||||
(let [val (subscribe [:query-id])] <-- reactive subscription
|
||||
(fn [something] <-- returns the render function
|
||||
[:div .... using @val in here])))
|
||||
```
|
||||
|
||||
There's no immediately obvious way to test this as a lovely pure function. Because it is not pure.
|
||||
|
||||
Of course, less pure ways are very possible. For example, a plan might be:
|
||||
1. setup `app-db` with some values in the right places (for the subscription)
|
||||
2. call `my-view` (with a parameter) which will return `the renderer`
|
||||
3. call `the renderer` (with a parameter) which will return hiccup
|
||||
4. check the hiccup structure for correctness.
|
||||
|
||||
Continuing on, in a second phase you could then:
|
||||
5. change the value in `app-db` (which will cause the subscription to fire)
|
||||
6. call `the renderer` again (hiccup returned).
|
||||
7. check that the hiccup
|
||||
|
||||
Which is all possible, if a little messy, and with one gotcha. After you change the value in `app-db` the subscription won't hold the new value straight away. It won't get calculated until the next animationFrame. And the next animationFrame won't happen until you hand back control to the browser. I think. Untested. Please report back here if you try. And you might also be able to use `reagent.core/flush` to force the view to be updated.
|
||||
|
||||
## Components - Part 2B
|
||||
|
||||
Or ... instead of the not-very-pure method above, you could use `with-redefs` on `subscribe` to stub out re-frame altogether:
|
||||
|
||||
```clj
|
||||
(defn subscription-stub [x]
|
||||
(atom
|
||||
(case x
|
||||
[:query-id] 42)))
|
||||
|
||||
(deftest some-test
|
||||
(with-redefs [re-frame/subscribe (subscription-stub)]
|
||||
(testing "some rendering"
|
||||
..... somehow call or render the component and check the output)))
|
||||
```
|
||||
|
||||
For more integration level testing, you can use `with-mounted-component` from the [reagent-template](https://github.com/reagent-project/reagent-template/blob/master/src/leiningen/new/reagent/test/cljs/reagent/core_test.cljs) to render the component in the browser and validate the generated DOM.
|
||||
|
||||
## Components - Part 2C
|
||||
|
||||
Or ... you can structure in the first place for easier testing and pure functions.
|
||||
|
||||
The trick here is to create an outer and inner component. The outer sources the data
|
||||
(via a subscription), and passes it onto the inner as props (parameters).
|
||||
|
||||
As a result, the inner component, which does the testable work, is pure and
|
||||
easily tested. The outer is fairly trivial.
|
||||
|
||||
To get a more concrete idea, I'll direct you to another page on this Wiki
|
||||
which has nothing to do with testing, but it does use this `simple-outer-subscribe-with-complicated-inner-render`
|
||||
pattern for a different purpose: [[Using-Stateful-JS-Components]]
|
||||
|
||||
Note this technique could be made simple and almost invisible via the
|
||||
use of macros. (Contribute one if you have it).
|
||||
|
||||
This pattern has been independently discovered by many. For example, here
|
||||
it is called the [Container/Component pattern](https://medium.com/@learnreact/container-components-c0e67432e005#.mb0hzgm3l).
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
So, we stumbled slightly at the final hurdle with Form-2 Components. But prior
|
||||
to this, the testing story for re-frame was as good as it gets: you are testing
|
||||
a bunch of simple, pure functions. No dependency injection in sight!
|
|
@ -1,3 +1,14 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [The re-frame Logo](#the-re-frame-logo)
|
||||
- [Who](#who)
|
||||
- [Genesis Theories](#genesis-theories)
|
||||
- [Assets Where?](#assets-where)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## The re-frame Logo
|
||||
|
||||
![logo](/images/logo/re-frame_256w.png?raw=true)
|
||||
|
|
|
@ -13,6 +13,19 @@ thrill of that forbidden fruit.
|
|||
|
||||
I won't tell, if you don't. But careful plans must be made ...
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [The overall plan](#the-overall-plan)
|
||||
- [Example Using Google Maps](#example-using-google-maps)
|
||||
- [Pattern Discovery](#pattern-discovery)
|
||||
- [Code Credit](#code-credit)
|
||||
- [D3 Examples](#d3-examples)
|
||||
- [Advanced Lifecycle Methods](#advanced-lifecycle-methods)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
|
||||
### The overall plan
|
||||
|
||||
|
@ -87,6 +100,8 @@ D3 (from @zachcp):
|
|||
- Code: https://github.com/zachcp/simplecomponent
|
||||
- Example: http://zachcp.github.io/simplecomponent/
|
||||
|
||||
A different take on using D3:
|
||||
https://gadfly361.github.io/gadfly-blog/2016-10-22-d3-in-reagent.html
|
||||
|
||||
### Advanced Lifecycle Methods
|
||||
|
||||
|
|
|
@ -0,0 +1,232 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [Flow](#flow)
|
||||
- [1 -> 2](#1---2)
|
||||
- [2 -> 3](#2---3)
|
||||
- [3->4->5->6](#3-4-5-6)
|
||||
- [On Flow](#on-flow)
|
||||
- [How Flow Happens In Reagent](#how-flow-happens-in-reagent)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Flow
|
||||
|
||||
It is time to talk of the reactive flow through dominoes 4-5-6. through dominoes It is time to talk about de-duplicated signal graphs.
|
||||
|
||||
|
||||
This tutorial focuses mainly on how data flows between dominoes 3-4-5-6.
|
||||
We'll look at the underlying reactive mechanism.
|
||||
|
||||
BUT we'll start by looking at the overall picture ...
|
||||
|
||||
|
||||
## Interconnections
|
||||
|
||||
Ask a Systems Theorist, and they'll tell you that a system has **parts** and **interconnections**.
|
||||
|
||||
Human brains tend to focus first on the **parts**, and then, later, maybe on
|
||||
**interconnections**. But we know better, right? We
|
||||
know interconnections are often critical to a system.
|
||||
"Focus on the lines between the boxes" we lecture anyone kind enough to listen (in my case, glassy-eyed family members).
|
||||
|
||||
In the case of re-frame, dominoes are the **parts**, so, tick, yes, we have
|
||||
looked at them first. Our brains are happy. But what about the **interconnections**?
|
||||
|
||||
If the **parts** are functions, what does it even mean to talk about **interconnections between functions?**
|
||||
To answer that question, I'll rephrase it as:
|
||||
how are the domino functions **composed**?
|
||||
|
||||
At the language level,
|
||||
Uncle Alonzo and Uncle John tell us how a function like `count` composes:
|
||||
```clj
|
||||
(str (count (filter odd? [1 2 3 4 5])))
|
||||
```
|
||||
We know when `count` is called, and with what
|
||||
argument, and how the value it computes becomes the arg for a further function.
|
||||
We know how data "flows" into and out of the functions.
|
||||
|
||||
Sometimes, we'd rewrite this code as:
|
||||
```clj
|
||||
(->> [1 2 3 4 5]
|
||||
(filter odd?)
|
||||
count
|
||||
str)
|
||||
```
|
||||
With this arrangement, we talk of "threading" data
|
||||
through functions. **It seems to help our comprehension to frame function
|
||||
composition in terms of data flow**.
|
||||
|
||||
re-frame delivers architecture
|
||||
by supplying the interconnections - it threads the data - it composes the dominoes - it is the lines between the boxes.
|
||||
|
||||
But re-frame has no universal method for this. The technique it uses varies from one domino neighbour
|
||||
pair to the next.
|
||||
|
||||
## Between 1 and 2
|
||||
|
||||
There's a queue.
|
||||
|
||||
When you `dispatch` an event, it is put into a FIFO queue to be processed "vey soon".
|
||||
|
||||
It is important to the design of re-frame that event processing is async.
|
||||
|
||||
On the end of the queue, is a `router` which (very soon) will:
|
||||
- pick up events one after the other
|
||||
- for each, it extracts `kind` of event (first element of the event vector)
|
||||
- for each, it looks up the associated event handler and calls it
|
||||
|
||||
|
||||
## Between 2 and 3
|
||||
|
||||
I lied above.
|
||||
|
||||
I said the `router` called the event handler associated with an event. This is a
|
||||
useful simplification, but we'll see in future tutorials that there's more going on.
|
||||
|
||||
I'll wave my hands about now and give you a sense of the real story.
|
||||
|
||||
Instead of there being a single handler function, there's actually a pipeline of functions which
|
||||
we call an interceptor chain. The handler you write is inserted into the middle of this pipeline.
|
||||
|
||||
This function pipeline manages three things:
|
||||
- it prepares the `coeffect` for the event handler (the set of inputs required by the handler)
|
||||
- it calls the event handler (Domino 2)
|
||||
- it handles the `effects` produced by the event handler (Domino 3)
|
||||
|
||||
|
||||
The router actually looks up the associated "interceptor chain", which happens to have the handler wrapped on the end.
|
||||
|
||||
And then it processes the interceptor chain. Which is to say it calls a
|
||||
|
||||
|
||||
There's
|
||||
- calls the handler , looks at their first , looks at their
|
||||
first element, and runs the associated
|
||||
|
||||
Between 1 and 2 it is a queue & router,
|
||||
between 2 and 3 it is an interceptor pipeline, and along the 3-4-5-6 domino axis there's a reactive signal graph. The right
|
||||
tool for the job in each case, I'd argue.
|
||||
|
||||
While interconnections are critical to how **re-frame works**,
|
||||
you can happily **use re-frame** for a long time and be mostly ignorant of their details.
|
||||
|
||||
Which is a good thing - back we go to happy brains focusing on the **parts**.
|
||||
|
||||
|
||||
|
||||
## 1 -> 2
|
||||
|
||||
`dispatch` queues events and they are not immediately processed. Event handling is done async.
|
||||
|
||||
A router reads events from this queue, looks up the associated handler and calls it.
|
||||
|
||||
## 2 -> 3
|
||||
|
||||
Except I lied in the previous section. The router doesn't really look
|
||||
up a single "handler". Instead it looks up an interceptor chain. The method by which
|
||||
an Interceptor chain is executed is discussed in great detail shortly.
|
||||
|
||||
|
||||
## 3->4->5->6
|
||||
|
||||
So now we are at the meat and potatoes. The real subject of this tutorial.
|
||||
|
||||
## On Flow
|
||||
|
||||
Arguments from authority ...
|
||||
|
||||
> Everything flows, nothing stands still. (Panta rhei)
|
||||
|
||||
> No man ever steps in the same river twice for it's not the same river and he's not the same man.
|
||||
|
||||
[Heraclitus 500 BC](http://en.wikiquote.org/wiki/Heraclitus). Who, being Greek, had never seen a frozen river. [alt version](http://farm6.static.flickr.com/5213/5477602206_ecb78559ed.jpg).
|
||||
|
||||
|
||||
> Think of an experience from your childhood. Something you remember clearly, something you can see,
|
||||
feel, maybe even smell, as if you were really there. After all you really were there at the time,
|
||||
weren’t you? How else could you remember it? But here is the bombshell: you weren’t there. Not a
|
||||
single atom that is in your body today was there when that event took place .... Matter flows
|
||||
from place to place and momentarily comes together to be you. Whatever you are, therefore, you
|
||||
are not the stuff of which you are made. If that does not make the hair stand up on the back of
|
||||
your neck, read it again until it does, because it is important.
|
||||
|
||||
Steve Grand
|
||||
|
||||
|
||||
### How Flow Happens In Reagent
|
||||
|
||||
To implement a reactive flow, Reagent provides a `ratom` and a `reaction`.
|
||||
re-frame uses both of these
|
||||
building blocks, so let's now make sure we understand them.
|
||||
|
||||
`ratoms` behave just like normal ClojureScript atoms. You can `swap!` and `reset!` them, `watch` them, etc.
|
||||
|
||||
From a ClojureScript perspective, the purpose of an atom is to hold mutable data. From a re-frame
|
||||
perspective, we'll tweak that paradigm slightly and **view a `ratom` as having a value that
|
||||
changes over time.** Seems like a subtle distinction, I know, but because of it, re-frame sees a
|
||||
`ratom` as a Signal. [Pause and read this](http://elm-lang.org:1234/guide/reactivity).
|
||||
|
||||
The 2nd building block, `reaction`, acts a bit like a function. It's a macro which wraps some
|
||||
`computation` (a block of code) and returns a `ratom` holding the result of that `computation`.
|
||||
|
||||
The magic thing about a `reaction` is that the `computation` it wraps will be automatically
|
||||
re-run whenever 'its inputs' change, producing a new output (return) value.
|
||||
|
||||
Eh, how?
|
||||
|
||||
Well, the `computation` is just a block of code, and if that code dereferences one or
|
||||
more `ratoms`, it will be automatically re-run (recomputing a new return value) whenever any
|
||||
of these dereferenced `ratoms` change.
|
||||
|
||||
To put that yet another way, a `reaction` detects a `computation's` input Signals (aka input `ratoms`)
|
||||
and it will `watch` them, and when, later, it detects a change in one of them, it will re-run that
|
||||
computation, and it will `reset!` the new result of that computation into the `ratom` originally returned.
|
||||
|
||||
So, the `ratom` returned by a `reaction` is itself a Signal. Its value will change over time when
|
||||
the `computation` is re-run.
|
||||
|
||||
So, via the interplay between `ratoms` and `reactions`, values 'flow' into computations and out
|
||||
again, and then into further computations, etc. "Values" flow (propagate) through the Signal graph.
|
||||
|
||||
But this Signal graph must be without cycles, because cycles cause mayhem! re-frame achieves
|
||||
a unidirectional flow.
|
||||
|
||||
Right, so that was a lot of words. Some code to clarify:
|
||||
|
||||
```Clojure
|
||||
(ns example1
|
||||
(:require-macros [reagent.ratom :refer [reaction]]) ;; reaction is a macro
|
||||
(:require [reagent.core :as reagent]))
|
||||
|
||||
(def app-db (reagent/atom {:a 1})) ;; our root ratom (signal)
|
||||
|
||||
(def ratom2 (reaction {:b (:a @app-db)})) ;; reaction wraps a computation, returns a signal
|
||||
(def ratom3 (reaction (condp = (:b @ratom2) ;; reaction wraps another computation
|
||||
0 "World"
|
||||
1 "Hello")))
|
||||
|
||||
;; Notice that both computations above involve de-referencing a ratom:
|
||||
;; - app-db in one case
|
||||
;; - ratom2 in the other
|
||||
;; Notice that both reactions above return a ratom.
|
||||
;; Those returned ratoms hold the (time varying) value of the computations.
|
||||
|
||||
(println @ratom2) ;; ==> {:b 1} ;; a computed result, involving @app-db
|
||||
(println @ratom3) ;; ==> "Hello" ;; a computed result, involving @ratom2
|
||||
|
||||
(reset! app-db {:a 0}) ;; this change to app-db, triggers re-computation
|
||||
;; of ratom2
|
||||
;; which, in turn, causes a re-computation of ratom3
|
||||
|
||||
(println @ratom2) ;; ==> {:b 0} ;; ratom2 is result of {:b (:a @app-db)}
|
||||
(println @ratom3) ;; ==> "World" ;; ratom3 is automatically updated too.
|
||||
```
|
||||
|
||||
So, in FRP-ish terms, a `reaction` will produce a "stream" of values over time (it is a Signal),
|
||||
accessible via the `ratom` it returns.
|
||||
|
||||
|
||||
Okay, that was all important background information for what is to follow.
|
||||
|
|
@ -0,0 +1,535 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table Of Contents
|
||||
|
||||
- [It solves a dilemma](#it-solves-a-dilemma)
|
||||
- [Implements Reactive Data Flows](#implements-reactive-data-flows)
|
||||
- [Flow](#flow)
|
||||
- [Reactive Programming](#reactive-programming)
|
||||
- [Components](#components)
|
||||
- [Truth Interlude](#truth-interlude)
|
||||
- [React etc.](#react-etc)
|
||||
- [Subscribe](#subscribe)
|
||||
- [The Signal Graph](#the-signal-graph)
|
||||
- [A More Efficient Signal Graph](#a-more-efficient-signal-graph)
|
||||
- [](#)
|
||||
- [Prefer Dumb Views - Part 1](#prefer-dumb-views---part-1)
|
||||
- [Prefer Dumb Views - Part 2](#prefer-dumb-views---part-2)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
|
||||
|
||||
## It solves a dilemma
|
||||
|
||||
SPAs are fundamentally mutative in nature.
|
||||
|
||||
They change the DOM, databases, localstore, cookies, send emails etc. Its a veritable frenzy of mutation. And
|
||||
this is a good thing. Any user of these SPAs wants to be changing the world,
|
||||
right, otherwise what's the point?
|
||||
|
||||
But we are wide-eyed functional zealots, heroically resisting the
|
||||
entire notion of mutation, and insisting instead on the wonders of pure functions.
|
||||
|
||||
re-frame solves this dilemma and allows you
|
||||
compose a mutative application from pure functions.
|
||||
|
||||
|
||||
XXX
|
||||
|
||||
I'll be using [Reagent] at an intermediate level, so you will need to have done some
|
||||
introductory Reagent tutorials before going on. Try:
|
||||
- [The Introductory Tutorial](http://reagent-project.github.io/) or
|
||||
- [this one](https://github.com/jonase/reagent-tutorial) or
|
||||
- [Building Single Page Apps with Reagent](http://yogthos.net/posts/2014-07-15-Building-Single-Page-Apps-with-Reagent.html).
|
||||
|
||||
## Implements Reactive Data Flows
|
||||
|
||||
This document describes how re-frame implements
|
||||
the reactive data flows in dominoes 4 and 5 (queries and views).
|
||||
|
||||
It explains
|
||||
the low level mechanics of the process which not something you
|
||||
need to know initially. So, you can defer reading and understanding
|
||||
this until later, if you wish. But you should at some point circle
|
||||
back and grok it. It isn't hard at all.
|
||||
|
||||
|
||||
|
||||
|
||||
## Flow
|
||||
|
||||
|
||||
|
||||
## Reactive Programming
|
||||
|
||||
|
||||
|
||||
We'll get to the meat in a second, I promise, but first one final, useful diversion ...
|
||||
|
||||
Terminology in the FRP world seems to get people hot under the collar. Those who believe in continuous-time
|
||||
semantics might object to me describing re-frame as having FRP-nature. They'd claim that it does something
|
||||
different from pure FRP, which is true.
|
||||
|
||||
But, these days, FRP seems to have become a
|
||||
["big tent"](http://soft.vub.ac.be/Publications/2012/vub-soft-tr-12-13.pdf)
|
||||
(a broad church?).
|
||||
Broad enough perhaps that re-frame can be in the far, top, left paddock of the tent, via a series of
|
||||
qualifications: re-frame has "discrete, dynamic, asynchronous, push FRP-ish-nature" without "glitch free" guarantees.
|
||||
(Surprisingly, "glitch" has specific meaning in FRP).
|
||||
|
||||
**If you are new to FRP, or reactive programming generally**, browse these resources before
|
||||
going further (certainly read the first two):
|
||||
- [Creative Explanation](http://paulstovell.com/blog/reactive-programming)
|
||||
- [Reactive Programming Backgrounder](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754)
|
||||
- [presentation (video)](http://www.infoq.com/presentations/ClojureScript-Javelin) by Alan Dipert (co-author of Hoplon)
|
||||
- [serious pants Elm thesis](https://www.seas.harvard.edu/sites/default/files/files/archived/Czaplicki.pdf)
|
||||
|
||||
|
||||
|
||||
|
||||
## Components
|
||||
|
||||
Extending the diagram, we introduce `components`:
|
||||
|
||||
```
|
||||
app-db --> components --> Hiccup
|
||||
```
|
||||
|
||||
When using Reagent, your primary job is to write one or more `components`.
|
||||
This is the view layer.
|
||||
|
||||
Think about `components` as `pure functions` - data in, Hiccup out. `Hiccup` is
|
||||
ClojureScript data structures which represent DOM. Here's a trivial component:
|
||||
|
||||
```Clojure
|
||||
(defn greet
|
||||
[]
|
||||
[:div "Hello ratoms and reactions"])
|
||||
```
|
||||
|
||||
And if we call it:
|
||||
```Clojure
|
||||
(greet)
|
||||
;; ==> [:div "Hello ratoms and reactions"]
|
||||
```
|
||||
|
||||
You'll notice that our component is a regular Clojure function, nothing special. In this case, it takes
|
||||
no parameters and it returns a ClojureScript vector (formatted as Hiccup).
|
||||
|
||||
Here is a slightly more interesting (parameterised) component (function):
|
||||
|
||||
```Clojure
|
||||
(defn greet ;; greet has a parameter now
|
||||
[name] ;; 'name' is a ratom holding a string
|
||||
[:div "Hello " @name]) ;; dereference 'name' to extract the contained value
|
||||
|
||||
;; create a ratom, containing a string
|
||||
(def n (reagent/atom "re-frame"))
|
||||
|
||||
;; call our `component` function, passing in a ratom
|
||||
(greet n)
|
||||
;; ==> [:div "Hello " "re-frame"] returns a vector
|
||||
```
|
||||
|
||||
So components are easy - at core they are a render function which turns data into
|
||||
Hiccup (which will later become DOM).
|
||||
|
||||
Now, let's introduce `reaction` into this mix. On the one hand, I'm complicating things
|
||||
by doing this, because Reagent allows you to be ignorant of the mechanics I'm about to show
|
||||
you. (It invisibly wraps your components in a `reaction` allowing you to be blissfully
|
||||
ignorant of how the magic happens.)
|
||||
|
||||
On the other hand, it is useful to understand exactly how the Reagent Signal graph is wired,
|
||||
because in a minute, when we get to `subscriptions`, we'll be directly using `reaction`, so we
|
||||
might as well bite the bullet here and now ... and, anyway, it is pretty easy...
|
||||
|
||||
```Clojure
|
||||
(defn greet ;; a component - data in, Hiccup out.
|
||||
[name] ;; name is a ratom
|
||||
[:div "Hello " @name]) ;; dereference name here, to extract the value within
|
||||
|
||||
(def n (reagent/atom "re-frame"))
|
||||
|
||||
;; The computation '(greet n)' returns Hiccup which is stored into 'hiccup-ratom'
|
||||
(def hiccup-ratom (reaction (greet n))) ;; <-- use of reaction !!!
|
||||
|
||||
;; what is the result of the initial computation ?
|
||||
(println @hiccup-ratom)
|
||||
;; ==> [:div "Hello " "re-frame"] ;; returns hiccup (a vector of stuff)
|
||||
|
||||
;; now change 'n'
|
||||
;; 'n' is an input Signal for the reaction above.
|
||||
;; Warning: 'n' is not an input signal because it is a parameter. Rather, it is
|
||||
;; because 'n' is dereferenced within the execution of the reaction's computation.
|
||||
;; reaction notices what ratoms are dereferenced in its computation, and watches
|
||||
;; them for changes.
|
||||
(reset! n "blah") ;; n changes
|
||||
|
||||
;; The reaction above will notice the change to 'n' ...
|
||||
;; ... and will re-run its computation ...
|
||||
;; ... which will have a new "return value"...
|
||||
;; ... which will be "reset!" into "hiccup-ratom"
|
||||
(println @hiccup-ratom)
|
||||
;; ==> [:div "Hello " "blah"] ;; yep, there's the new value
|
||||
```
|
||||
|
||||
So, as `n` changes value over time (via a `reset!`), the output of the computation `(greet n)`
|
||||
changes, which in turn means that the value in `hiccup-ratom` changes. Both `n` and
|
||||
`hiccup-ratom` are FRP Signals. The Signal graph we created causes data to flow from
|
||||
`n` into `hiccup-ratom`.
|
||||
|
||||
Derived Data, flowing.
|
||||
|
||||
|
||||
### Truth Interlude
|
||||
|
||||
I haven't been entirely straight with you:
|
||||
|
||||
1. Reagent re-runs `reactions` (re-computations) via requestAnimationFrame. So a
|
||||
re-computation happens about 16ms after an input Signals change is detected, or after the
|
||||
current thread of processing finishes, whichever is the greater. So if you are in a bREPL
|
||||
and you run the lines of code above one after the other too quickly, you might not see the
|
||||
re-computation done immediately after `n` gets reset!, because the next animationFrame
|
||||
hasn't run (yet). But you could add a `(reagent.core/flush)` after the reset! to force
|
||||
re-computation to happen straight away.
|
||||
|
||||
2. `reaction` doesn't actually return a `ratom`. But it returns something that has
|
||||
ratom-nature, so we'll happily continue believing it is a `ratom` and no harm will come to us.
|
||||
|
||||
On with the rest of my lies and distortions...
|
||||
|
||||
|
||||
### React etc.
|
||||
|
||||
Okay, so we have some unidirectional, dynamic, async, discrete FRP-ish data flow happening here.
|
||||
|
||||
Question: To which ocean does this river of data flow? Answer: The DOM ocean.
|
||||
|
||||
The full picture:
|
||||
```
|
||||
app-db --> components --> Hiccup --> Reagent --> VDOM --> React --> DOM
|
||||
```
|
||||
|
||||
Best to imagine this process as a pipeline of 3 functions. Each
|
||||
function takes data from the
|
||||
previous step, and produces (derived!) data for the next step. In the next
|
||||
diagram, the three functions are marked (f1, f2, f3). The unmarked nodes are derived data,
|
||||
produced by one step, to be input to the following step. Hiccup,
|
||||
VDOM and DOM are all various forms of HTML markup (in our world that's data).
|
||||
|
||||
```
|
||||
app-db --> components --> Hiccup --> Reagent --> VDOM --> React --> DOM
|
||||
f1 f2 f3
|
||||
```
|
||||
|
||||
In abstract ClojureScript syntax terms, you could squint and imagine the process as:
|
||||
|
||||
```Clojure
|
||||
(-> app-db
|
||||
components ;; produces Hiccup
|
||||
Reagent ;; produces VDOM (virtual DOM that React understands)
|
||||
React ;; produces HTML (which magically and efficiently appears on the page).
|
||||
Browser ;; produces pixels
|
||||
Monitor) ;; produces photons?
|
||||
```
|
||||
|
||||
|
||||
Via the interplay between `ratom` and `reaction`, changes to `app-db` stream into the pipeline, where it
|
||||
undergoes successive transformations, until pixels colour the monitor you to see.
|
||||
|
||||
Derived Data, flowing. Every step is acting like a pure function and turning data into new data.
|
||||
|
||||
All well and good, and nice to know, but we don't have to bother ourselves with most of the pipeline.
|
||||
We just write the `components`
|
||||
part and Reagent/React will look after the rest. So back we go to that part of the picture ...
|
||||
|
||||
|
||||
## Subscribe
|
||||
|
||||
|
||||
|
||||
`components` (view layer) need to query aspects of `app-db` (data layer).
|
||||
|
||||
But how?
|
||||
|
||||
Let's pause to consider **our dream solution** for this part of the flow. `components` would:
|
||||
* obtain data from `app-db` (their job is to turn this data into hiccup).
|
||||
* obtain this data via a (possibly parameterised) query over `app-db`. Think database kind of query.
|
||||
* automatically recompute their hiccup output, as the data returned by the query changes, over time
|
||||
* use declarative queries. Components should know as little as possible about the structure of `app-db`. SQL? Datalog?
|
||||
|
||||
re-frame's `subscriptions` are an attempt to live this dream. As you'll see, they fall short on the declarative
|
||||
query part, but they comfortably meet the other requirements.
|
||||
|
||||
As a re-frame app developer, your job will be to write and register one or more
|
||||
"subscription handlers" - functions that do a named query.
|
||||
|
||||
Your subscription functions must return a value that changes over time (a Signal). I.e. they'll
|
||||
be returning a reaction or, at least, the `ratom` produced by a `reaction`.
|
||||
|
||||
Rules:
|
||||
- `components` never source data directly from `app-db`, and instead, they use a subscription.
|
||||
- subscriptions are only ever used by components (they are never used in, say, event handlers).
|
||||
|
||||
Here's a component using a subscription:
|
||||
|
||||
```Clojure
|
||||
(defn greet ;; outer, setup function, called once
|
||||
[]
|
||||
(let [name-ratom (subscribe [:name-query])] ;; <---- subscribing happens here
|
||||
(fn [] ;; the inner, render function, potentially called many times.
|
||||
[:div "Hello" @name-ratom])))
|
||||
```
|
||||
|
||||
First, note this is a [Form-2](https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components#form-2--a-function-returning-a-function)
|
||||
`component` ([there are 3 forms](https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components)).
|
||||
|
||||
Previously in this document, we've used the simplest, `Form-1` components (no setup was required, just render).
|
||||
With `Form-2` components, there's a function returning a function:
|
||||
- the returned function is the render function. Behind the scenes, Reagent will wrap this render function
|
||||
in a `reaction` to make it produce new Hiccup when its input Signals change. In our example above, that
|
||||
means it will rerun every time `name-ratom` changes.
|
||||
- the outer function is a setup function, called once for each instance of the component. Notice the use of
|
||||
'subscribe' with the parameter `:name-query`. That creates a Signal through which new values are supplied
|
||||
over time; each new value causing the returned function (the actual renderer) to be run.
|
||||
|
||||
>It is important to distinguish between a new instance of the component versus the same instance of a component reacting to a new value. Simplistically, a new component is returned for every unique value the setup function (i.e. the outer function) is called with. This allows subscriptions based on initialisation values to be created, for example:
|
||||
``` Clojure
|
||||
(defn my-cmp [row-id]
|
||||
(let [row-state (subscribe [row-id])]
|
||||
(fn [row-id]
|
||||
[:div (str "Row: " row-id " is " @row-state)])))
|
||||
```
|
||||
In this example, `[my-cmp 1][my-cmp 2]` will create two instances of `my-cmp`. Each instance will re-render when its internal `row-state` signal changes.
|
||||
|
||||
`subscribe` is always called like this:
|
||||
|
||||
```Clojure
|
||||
(subscribe [query-id some optional query parameters])
|
||||
```
|
||||
|
||||
There is only one (global) `subscribe` function and it takes one parameter, assumed to be a vector.
|
||||
|
||||
The first element in the vector (shown as `query-id` above) identifies/names the query and the other elements are optional
|
||||
query parameters. With a traditional database a query might be:
|
||||
|
||||
```
|
||||
select * from customers where name="blah"
|
||||
```
|
||||
|
||||
In re-frame, that would be done as follows:
|
||||
`(subscribe [:customer-query "blah"])`
|
||||
which would return a `ratom` holding the customer state (a value which might change over time!).
|
||||
|
||||
So let's now look at how to write and register the subscription handler for `:customer-query`
|
||||
|
||||
```Clojure
|
||||
(defn customer-query ;; a query over 'app-db' which returns a customer
|
||||
[db, [sid cid]] ;; query fns are given 'app-db', plus vector given to subscribe
|
||||
(assert (= sid :customer-query)) ;; subscription id was the first element in the vector
|
||||
(reaction (get-in @db [:path :to :a :map cid]))) ;; re-runs each time db changes
|
||||
|
||||
;; register our query handler
|
||||
(register-sub
|
||||
:customer-query ;; the id (the name of the query)
|
||||
customer-query) ;; the function which will perform the query
|
||||
```
|
||||
|
||||
Notice how the handler is registered to handle `:customer-query` subscriptions.
|
||||
|
||||
**Rules and Notes**:
|
||||
- you'll be writing one or more handlers, and you will need to register each one.
|
||||
- handlers are functions which take two parameters: the db atom, and the vector given to subscribe.
|
||||
- `components` tend to be organised into a hierarchy, often with data flowing from parent to child via
|
||||
parameters. So not every component needs a subscription. Very often the values passed in from a parent component
|
||||
are sufficient.
|
||||
- subscriptions can only be used in `Form-2` components and the subscription must be in the outer setup
|
||||
function and not in the inner render function. So the following is **wrong** (compare to the correct version above)
|
||||
|
||||
```Clojure
|
||||
(defn greet ;; a Form-1 component - no inner render function
|
||||
[]
|
||||
(let [name-ratom (subscribe [:name-query])] ;; Eek! subscription in renderer
|
||||
[:div "Hello" @name-ratom]))
|
||||
```
|
||||
|
||||
Why is this wrong? Well, this component would be re-rendered every time `app-db` changed, even if the value
|
||||
in `name-ratom` (the result of the query) stayed the same. If you were to use a `Form-2` component instead, and put the
|
||||
subscription in the outer functions, then there'll be no re-render unless the value queried (i.e. `name-ratom`) changed.
|
||||
|
||||
|
||||
## The Signal Graph
|
||||
|
||||
Let's sketch out the situation described above ...
|
||||
|
||||
`app-db` would be a bit like this (`items` is a vector of maps):
|
||||
```Clojure
|
||||
(def L [{:name "a" :val 23 :flag "y"}
|
||||
{:name "b" :val 81 :flag "n"}
|
||||
{:name "c" :val 23 :flag "y"}])
|
||||
|
||||
(def app-db (reagent/atom {:items L
|
||||
:sort-by :name})) ;; sorted by the :name attribute
|
||||
```
|
||||
|
||||
The subscription-handler might be written:
|
||||
|
||||
```Clojure
|
||||
(register-sub
|
||||
:sorted-items ;; the query id (the name of the query)
|
||||
(fn [db [_]] ;; the handler for the subscription
|
||||
(reaction
|
||||
(let [items (get-in @db [:items]) ;; extract items from db
|
||||
sort-attr (get-in @db [:sort-by])] ;; extract sort key from db
|
||||
(sort-by sort-attr items))))) ;; return them sorted
|
||||
```
|
||||
|
||||
|
||||
Subscription handlers are given two parameters:
|
||||
|
||||
1. `app-db` - that's a reagent/atom which holds ALL the app's state. This is the "database"
|
||||
on which we perform the "query".
|
||||
2. the vector originally supplied to `subscribe`. In our case, we ignore it.
|
||||
|
||||
In the example above, notice that the `reaction` depends on the input Signal: `db`.
|
||||
If `db` changes, the query is re-run.
|
||||
|
||||
In a component, we could use this query via `subscribe`:
|
||||
|
||||
```Clojure
|
||||
(defn items-list ;; Form-2 component - outer, setup function, called once
|
||||
[]
|
||||
(let [items (subscribe [:sorted-items]) ;; <-- subscribe called with name
|
||||
num (reaction (count @items)) ;; Woh! a reaction based on the subscription
|
||||
top-20 (reaction (take 20 @items))] ;; Another dependent reaction
|
||||
(fn []
|
||||
[:div
|
||||
(str "there's " @num " of these suckers. Here's top 20") ;; rookie mistake to leave off the @
|
||||
(into [:div ] (map item-render @top-20))]))) ;; item-render is another component, not shown
|
||||
```
|
||||
|
||||
There's a bit going on in that `let`, most of it tortuously contrived, just so I can show off chained
|
||||
reactions. Okay, okay, all I wanted really was an excuse to use the phrase "chained reactions".
|
||||
|
||||
The calculation of `num` is done by a `reaction` which has `items` as an input Signal. And,
|
||||
as we saw, `items` is itself a reaction over two other signals (one of them the `app-db`).
|
||||
|
||||
So this is a Signal Graph. Data is flowing through computation into renderer, which produce Hiccup, etc.
|
||||
|
||||
## A More Efficient Signal Graph
|
||||
|
||||
But there is a small problem. The approach above might get inefficient, if `:items` gets long.
|
||||
|
||||
Every time `app-db` changes, the `:sorted-items` query is
|
||||
going to be re-run and it's going to re-sort `:items`. But `:items` might not have changed. Some other
|
||||
part of `app-db` may have changed.
|
||||
|
||||
We don't want to perform this computationally expensive re-sort
|
||||
each time something unrelated in `app-db` changes.
|
||||
|
||||
Luckily, we can easily fix that up by tweaking our subscription function so
|
||||
that it chains `reactions`:
|
||||
|
||||
```Clojure
|
||||
(register-sub
|
||||
:sorted-items ;; the query id
|
||||
(fn [db [_]]
|
||||
(let [items (reaction (get-in @db [:some :path :to :items]))] ;; reaction #1
|
||||
sort-attr (reaction (get-in @db [:sort-by]))] ;; reaction #2
|
||||
(reaction (sort-by @sort-attr @items))))) ;; reaction #3
|
||||
```
|
||||
|
||||
The original version had only one `reaction` which would be re-run completely each time `app-db` changed.
|
||||
This new version, has chained reactions.
|
||||
The 1st and 2nd reactions just extract from `db`. They will run each time `app-db` changes.
|
||||
But they are cheap. The 3rd one does the expensive
|
||||
computation using the result from the first two.
|
||||
|
||||
That 3rd, expensive reaction will be re-run when either one of its two input Signals change, right? Not quite.
|
||||
`reaction` will only re-run the computation when one of the inputs has **changed in value**.
|
||||
|
||||
`reaction` compares the old input Signal value with the new Signal value using `identical?`. Because we're
|
||||
using immutable data structures
|
||||
(thank you ClojureScript), `reaction` can perform near instant checks for change on even
|
||||
deeply nested and complex
|
||||
input Signals. And `reaction` will then stop unneeded propagation of `identical?` values through the
|
||||
Signal graph.
|
||||
|
||||
In the example above, reaction #3 won't re-run until `:items` or `:sort-by` are different
|
||||
(do not test `identical?`
|
||||
to their previous value), even though `app-db` itself has changed (presumably somewhere else).
|
||||
|
||||
Hideously contrived example, but I hope you get the idea. It is all screamingly efficient.
|
||||
|
||||
Summary:
|
||||
- you can chain reactions.
|
||||
- a reaction will only be re-run when its input Signals test not `identical?` to previous value.
|
||||
- As a result, unnecessary Signal propagation is eliminated using highly efficient checks,
|
||||
even for large, deep nested data structures.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Back to the more pragmatic world ...
|
||||
|
||||
|
||||
|
||||
[SPAs]:http://en.wikipedia.org/wiki/Single-page_application
|
||||
[SPA]:http://en.wikipedia.org/wiki/Single-page_application
|
||||
[Reagent]:http://reagent-project.github.io/
|
||||
[Dan Holmsand]:https://twitter.com/holmsand
|
||||
[Flux]:http://facebook.github.io/flux/docs/overview.html#content
|
||||
[Hiccup]:https://github.com/weavejester/hiccup
|
||||
[FRP]:https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
|
||||
[Elm]:http://elm-lang.org/
|
||||
[OM]:https://github.com/swannodette/om
|
||||
[Prismatic Schema]:https://github.com/Prismatic/schema
|
||||
[Hoplon]:http://hoplon.io/
|
||||
[Pedestal App]:https://github.com/pedestal/pedestal-app
|
||||
|
||||
|
||||
-----------------
|
||||
|
||||
## Prefer Dumb Views - Part 1
|
||||
|
||||
Many events are dispatched by the DOM in response to user actions.
|
||||
|
||||
For example, a button view might be like this:
|
||||
```clj
|
||||
(defn yes-button
|
||||
[]
|
||||
[:div {:class "button-class"
|
||||
:on-click #(dispatch [:yes-button-clicked])}
|
||||
"Yes"])
|
||||
```
|
||||
|
||||
Notice that `on-click` DOM handler:
|
||||
```clj
|
||||
#(dispatch [:yes-button-clicked])
|
||||
```
|
||||
|
||||
With re-frame, we want the DOM as passive as possible. We do
|
||||
not want our views containing any imperative control logic.
|
||||
All of that should be computed by event handlers.
|
||||
|
||||
We want that "on-click" as simple as we can make it.
|
||||
|
||||
**Rule**: `views` are as passive and minimal as possible when it
|
||||
comes to handling events. They `dispatch` pure data and nothing more.
|
||||
|
||||
## Prefer Dumb Views - Part 2
|
||||
|
||||
Neither do we want views computing the data they render.
|
||||
That's the job of a subscription:
|
||||
|
||||
So this is bad:
|
||||
```clj
|
||||
(defn show-items
|
||||
[]
|
||||
(let [sorted-items (sort @(subscribe [:items]))] ;; <--
|
||||
(into [:div] (for [i sorted-items] [item-view i]))))
|
||||
```
|
||||
|
||||
The view is not simply taking the data supplied by the
|
|
@ -1,9 +1,24 @@
|
|||
# Reagent example app now using re-frame
|
||||
# A Simple App
|
||||
|
||||
Run "`lein do clean, figwheel`" in a terminal to compile the app, and then open `http://localhost:3449/example.html`.
|
||||
This tiny application is meant to provide a quick start of the basics of re-frame.
|
||||
|
||||
Any changes to ClojureScript source files (in `src`) will be reflected in the running page immediately (while "`lein figwheel`" is running).
|
||||
All the code is in one namespace `/src/simpleexample/core.cljs`
|
||||
|
||||
Run "`lein do clean, with-profile prod compile`" to compile an optimized version, and then open `resources/public/example.html`.
|
||||
### Run It And Change It
|
||||
|
||||
Original reagent example code found at https://github.com/reagent-project/reagent
|
||||
Steps:
|
||||
|
||||
A. Check out the re-frame repo
|
||||
1. Get a command line
|
||||
2. `cd` to the root of this sub project (where this README exists)
|
||||
3. run "`lein do clean, figwheel`" to compile the app,
|
||||
4. open `http://localhost:3449/example.html` to see the app
|
||||
|
||||
Whileever step 3 is running, any changes you make to the ClojureScript
|
||||
source files (in `src`) will be re-compiled and reflected in the running
|
||||
page immediately.
|
||||
|
||||
### Production Version
|
||||
|
||||
Run "`lein do clean, with-profile prod compile`" to compile an optimized
|
||||
version, and then open `resources/public/example.html`.
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
(ns simpleexample.dev
|
||||
(:require [simpleexample.core :as example]
|
||||
[figwheel.client :as fw]))
|
||||
|
||||
(fw/start {:on-jsload example/run
|
||||
:websocket-url "ws://localhost:3449/figwheel-ws"})
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
(defproject simple-re-frame "0.8.0"
|
||||
(defproject simple "0.8.0"
|
||||
:dependencies [[org.clojure/clojure "1.8.0"]
|
||||
[org.clojure/clojurescript "1.9.227"]
|
||||
[reagent "0.6.0-rc"]
|
||||
|
@ -10,8 +10,8 @@
|
|||
:hooks [leiningen.cljsbuild]
|
||||
|
||||
:profiles {:dev {:cljsbuild
|
||||
{:builds {:client {:source-paths ["devsrc"]
|
||||
:compiler {:main "simpleexample.dev"
|
||||
{:builds {:client {:figwheel {:on-jsload "simple.core/run"}
|
||||
:compiler {:main "simple.core"
|
||||
:asset-path "js"
|
||||
:optimizations :none
|
||||
:source-map true
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<script src="js/client.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
simpleexample.core.run();
|
||||
simple.core.run();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
(ns simple.core
|
||||
(:require [reagent.core :as reagent]
|
||||
[re-frame.core :as rf]))
|
||||
|
||||
;; -- Domino 1 - Event Dispatch -----------------------------------------------
|
||||
|
||||
(defn dispatch-timer-event
|
||||
[]
|
||||
(let [now (js/Date.)]
|
||||
(rf/dispatch [:timer now]))) ;; <-- dispatch used
|
||||
|
||||
;; Call the dispatching function every second.
|
||||
;; `defonce` is like `def` but it ensures only instance is ever
|
||||
;; created in the face of figwheel hot-reloading of this file.
|
||||
(defonce do-timer (js/setInterval dispatch-timer-event 1000))
|
||||
|
||||
|
||||
;; -- Domino 2 - Event Handlers -----------------------------------------------
|
||||
|
||||
(rf/reg-event-db ;; sets up initial application state
|
||||
:initialize ;; usage: (dispatch [:initialize])
|
||||
(fn [_ _] ;; the two parameters are not important here, so use _
|
||||
{:time (js/Date.) ;; What it returns becomes the new application state
|
||||
:time-color "#f88"})) ;; so the application state will initially be a map with two keys
|
||||
|
||||
|
||||
(rf/reg-event-db ;; usage: (dispatch [:time-color-change 34562])
|
||||
:time-color-change ;; dispatched when the user enters a new colour into the UI
|
||||
(fn [db [_ new-color-value]] ;; -db event handlers given 2 parameters: current application state and event (a vector)
|
||||
(assoc db :time-color new-color-value))) ;; compute and return the new application state
|
||||
|
||||
|
||||
(rf/reg-event-db ;; usage: (dispatch [:timer a-js-Date])
|
||||
:timer ;; every second an event of this kind will be dispatched
|
||||
(fn [db [_ new-time]] ;; note how the 2nd parameter is desctructure to obtain the data value
|
||||
(assoc db :time new-time))) ;; compute and return the new application state
|
||||
|
||||
|
||||
;; -- Domino 4 - Query -------------------------------------------------------
|
||||
|
||||
(rf/reg-sub
|
||||
:time
|
||||
(fn [db _] ;; db is current app state. 2nd usused param is query vector
|
||||
(-> db
|
||||
:time)))
|
||||
|
||||
(rf/reg-sub
|
||||
:time-color
|
||||
(fn [db _]
|
||||
(:time-color db)))
|
||||
|
||||
|
||||
;; -- Domino 5 - View Functions ----------------------------------------------
|
||||
|
||||
(defn clock
|
||||
[]
|
||||
[:div.example-clock
|
||||
{:style {:color @(rf/subscribe [:time-color])}}
|
||||
(-> @(rf/subscribe [:time])
|
||||
.toTimeString
|
||||
(clojure.string/split " ")
|
||||
first)])
|
||||
|
||||
(defn color-input
|
||||
[]
|
||||
[:div.color-input
|
||||
"Time color: "
|
||||
[:input {:type "text"
|
||||
:value @(rf/subscribe [:time-color])
|
||||
:on-change #(rf/dispatch [:time-color-change (-> % .-target .-value)])}]]) ;; <---
|
||||
|
||||
(defn ui
|
||||
[]
|
||||
[:div
|
||||
[:h1 "Hello world, it is now"]
|
||||
[clock]
|
||||
[color-input]])
|
||||
|
||||
;; -- Entry Point -------------------------------------------------------------
|
||||
|
||||
(defn ^:export run
|
||||
[]
|
||||
(rf/dispatch-sync [:initialize]) ;; puts a value into application state
|
||||
(reagent/render [ui] ;; mount the application's ui into '<div id="app" />'
|
||||
(js/document.getElementById "app")))
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
(ns simpleexample.core
|
||||
(:require-macros [reagent.ratom :refer [reaction]])
|
||||
(:require [reagent.core :as reagent]
|
||||
[re-frame.core :refer [reg-event-db
|
||||
path
|
||||
reg-sub
|
||||
dispatch
|
||||
dispatch-sync
|
||||
subscribe]]))
|
||||
|
||||
;; trigger a dispatch every second
|
||||
(defonce time-updater (js/setInterval
|
||||
#(dispatch [:timer (js/Date.)]) 1000))
|
||||
|
||||
(def initial-state
|
||||
{:timer (js/Date.)
|
||||
:time-color "#f88"})
|
||||
|
||||
|
||||
;; -- Event Handlers ----------------------------------------------------------
|
||||
|
||||
|
||||
(reg-event-db ;; setup initial state
|
||||
:initialize ;; usage: (dispatch [:initialize])
|
||||
(fn
|
||||
[db _]
|
||||
(merge db initial-state))) ;; what it returns becomes the new state
|
||||
|
||||
|
||||
(reg-event-db
|
||||
:time-color ;; usage: (dispatch [:time-color 34562])
|
||||
(path [:time-color]) ;; this is middleware
|
||||
(fn
|
||||
[time-color [_ value]] ;; path middleware adjusts the first parameter
|
||||
value))
|
||||
|
||||
|
||||
(reg-event-db
|
||||
:timer
|
||||
(fn
|
||||
;; the first item in the second argument is :timer the second is the
|
||||
;; new value
|
||||
[db [_ value]]
|
||||
(assoc db :timer value))) ;; return the new version of db
|
||||
|
||||
|
||||
;; -- Subscription Handlers ---------------------------------------------------
|
||||
|
||||
|
||||
(reg-sub
|
||||
:timer
|
||||
(fn
|
||||
[db _] ;; db is the value currently in the app-db atom
|
||||
(:timer db)))
|
||||
|
||||
|
||||
(reg-sub
|
||||
:time-color
|
||||
(fn
|
||||
[db _]
|
||||
(:time-color db)))
|
||||
|
||||
|
||||
;; -- View Components ---------------------------------------------------------
|
||||
|
||||
(defn greeting
|
||||
[message]
|
||||
[:h1 message])
|
||||
|
||||
|
||||
(defn clock
|
||||
[]
|
||||
(let [time-color (subscribe [:time-color])
|
||||
timer (subscribe [:timer])]
|
||||
(fn clock-render
|
||||
[]
|
||||
(let [time-str (-> @timer
|
||||
.toTimeString
|
||||
(clojure.string/split " ")
|
||||
first)
|
||||
style {:style {:color @time-color}}]
|
||||
[:div.example-clock style time-str]))))
|
||||
|
||||
|
||||
(defn color-input
|
||||
[]
|
||||
(let [time-color (subscribe [:time-color])]
|
||||
(fn color-input-render
|
||||
[]
|
||||
[:div.color-input
|
||||
"Time color: "
|
||||
[:input {:type "text"
|
||||
:value @time-color
|
||||
:on-change #(dispatch
|
||||
[:time-color (-> % .-target .-value)])}]])))
|
||||
|
||||
(defn simple-example
|
||||
[]
|
||||
[:div
|
||||
[greeting "Hello world, it is now"]
|
||||
[clock]
|
||||
[color-input]])
|
||||
|
||||
|
||||
;; -- Entry Point -------------------------------------------------------------
|
||||
|
||||
|
||||
(defn ^:export run
|
||||
[]
|
||||
(dispatch-sync [:initialize])
|
||||
(reagent/render [simple-example]
|
||||
(js/document.getElementById "app")))
|
||||
|
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 9.7 KiB |
After Width: | Height: | Size: 346 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 8.8 KiB |
|
@ -7,8 +7,8 @@ module.exports = function (config) {
|
|||
browsers: ['Chrome'],
|
||||
files: [
|
||||
root + '/../test.js', // same as :output-to
|
||||
{pattern: root + '/../test.js.map', included: false},
|
||||
{pattern: root + '/**/*.+(cljs|cljc|clj|js|js.map)', included: false}
|
||||
{pattern: root + '/../test.js.map', included: false, watched: false},
|
||||
{pattern: root + '/**/*.+(cljs|cljc|clj|js|js.map)', included: false, watched: false}
|
||||
],
|
||||
|
||||
client: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Michael Thompson
|
||||
Copyright (c) 2015-2016 Michael Thompson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
10
project.clj
|
@ -1,4 +1,4 @@
|
|||
(defproject re-frame "0.8.1"
|
||||
(defproject re-frame "0.9.0-SNAPSHOT"
|
||||
:description "A Clojurescript MVC-like Framework For Writing SPAs Using Reagent."
|
||||
:url "https://github.com/Day8/re-frame.git"
|
||||
:license {:name "MIT"}
|
||||
|
@ -47,7 +47,7 @@
|
|||
:cljsbuild {:builds [{:id "test"
|
||||
:source-paths ["test" "src"]
|
||||
:compiler {:preloads [devtools.preload]
|
||||
:external-config {:devtools/config {:features-to-install :all}}
|
||||
:external-config {:devtools/config {:features-to-install [:formatters :hints]}}
|
||||
:output-to "run/compiled/browser/test.js"
|
||||
:source-map true
|
||||
:output-dir "run/compiled/browser/test"
|
||||
|
@ -61,8 +61,10 @@
|
|||
:output-dir "run/compiled/karma/test"
|
||||
:optimizations :whitespace
|
||||
:main "re_frame.test_runner"
|
||||
:pretty-print true}}]}
|
||||
:pretty-print true
|
||||
:closure-defines {"re_frame.trace.trace_enabled_QMARK_" true}}}]}
|
||||
|
||||
:aliases {"test-once" ["do" "clean," "cljsbuild" "once" "test," "shell" "open" "test/test.html"]
|
||||
"test-auto" ["do" "clean," "cljsbuild" "auto" "test,"]
|
||||
"karma-once" ["do" "clean," "cljsbuild" "once" "karma,"]})
|
||||
"karma-once" ["do" "clean," "cljsbuild" "once" "karma,"]
|
||||
"karma-auto" ["do" "clean," "cljsbuild" "auto" "karma,"]})
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
Given an `id`, and an optional value, lookup the registered coeffect
|
||||
handler (previously registered via `reg-cofx`) and it with two arguments:
|
||||
the current value of `:coeffect` and, optionally, the value. The registered handler
|
||||
the current value of `:coeffects` and, optionally, the value. The registered handler
|
||||
is expected to return a modified coeffect.
|
||||
"
|
||||
([id]
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
[re-frame.interceptor :as interceptor]
|
||||
[re-frame.std-interceptors :as std-interceptors :refer [db-handler->interceptor
|
||||
fx-handler->interceptor
|
||||
ctx-handler->interceptor]]))
|
||||
ctx-handler->interceptor]]
|
||||
[clojure.set :as set]))
|
||||
|
||||
|
||||
;; -- dispatch
|
||||
|
@ -45,11 +46,11 @@
|
|||
|
||||
;; -- subscriptions
|
||||
(defn reg-sub-raw
|
||||
"Associate a given `query id` with a given subscription handler fucntion `handler-fn`
|
||||
"Associate a given `query id` with a given subscription handler function `handler-fn`
|
||||
which is expected to take two arguments: app-db and query vector, and return
|
||||
a `reaction`.
|
||||
|
||||
This is a low level, advanced function. You should probably be using re-sub
|
||||
This is a low level, advanced function. You should probably be using reg-sub
|
||||
instead."
|
||||
[query-id handler-fn]
|
||||
(registrar/register-handler subs/kind query-id handler-fn))
|
||||
|
@ -58,6 +59,7 @@
|
|||
(def subscribe subs/subscribe)
|
||||
|
||||
(def clear-sub (partial registrar/clear-handlers subs/kind))
|
||||
(def clear-subscription-cache! subs/clear-subscription-cache!)
|
||||
|
||||
;; -- effects
|
||||
(def reg-fx fx/register)
|
||||
|
@ -133,14 +135,14 @@
|
|||
(fn []
|
||||
;; call `dispose!` on all current subscriptions which
|
||||
;; didn't originally exist.
|
||||
#_(->> subs/query->reaction
|
||||
vals
|
||||
(remove (set (vals subs-cache))) ;;
|
||||
(map interop/dispose!)
|
||||
(doall))
|
||||
(let [original-subs (set (vals subs-cache))
|
||||
current-subs (set (vals @subs/query->reaction))]
|
||||
(doseq [sub (set/difference current-subs original-subs)]
|
||||
(interop/dispose! sub)))
|
||||
|
||||
;; reset the atoms
|
||||
(reset! subs/query->reaction subs-cache)
|
||||
;; Reset the atoms
|
||||
;; We don't need to reset subs/query->reaction, as
|
||||
;; disposing of the subs removes them from the cache anyway
|
||||
(reset! registrar/kind->id->handler handlers)
|
||||
(reset! db/app-db app-db)
|
||||
nil)))
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
[re-frame.interop :refer [empty-queue debug-enabled?]]
|
||||
[re-frame.registrar :refer [get-handler register-handler]]
|
||||
[re-frame.loggers :refer [console]]
|
||||
[re-frame.interceptor :as interceptor]))
|
||||
[re-frame.interceptor :as interceptor]
|
||||
[re-frame.trace :as trace :include-macros true]))
|
||||
|
||||
|
||||
(def kind :event)
|
||||
|
@ -56,6 +57,9 @@
|
|||
(if *handling*
|
||||
(console :error (str "re-frame: while handling \"" *handling* "\", dispatch-sync was called for \"" event-v "\". You can't call dispatch-sync within an event handler."))
|
||||
(binding [*handling* event-v]
|
||||
(interceptor/execute event-v interceptors))))))
|
||||
(trace/with-trace {:operation event-id
|
||||
:op-type kind
|
||||
:tags {:event event-v}}
|
||||
(interceptor/execute event-v interceptors)))))))
|
||||
|
||||
|
||||
|
|
|
@ -84,9 +84,9 @@
|
|||
Returns updated `context`. Ie. the `context` which has been threaded
|
||||
through all interceptor functions.
|
||||
|
||||
Generally speaking, an interceptor's `:before` fucntion will (if present)
|
||||
add to a `context's` `:coeffect`, while it's `:after` function
|
||||
will modify the `context`'s `:effect`. Very approximately.
|
||||
Generally speaking, an interceptor's `:before` function will (if present)
|
||||
add to a `context's` `:coeffects`, while it's `:after` function
|
||||
will modify the `context`'s `:effects`. Very approximately.
|
||||
|
||||
But because all interceptor functions are given `context`, and can
|
||||
return a modified version of it, the way is clear for an interceptor
|
||||
|
@ -109,7 +109,7 @@
|
|||
"Add a collection of `interceptors` to the end of `context's` execution `:queue`.
|
||||
Returns the updated `context`.
|
||||
|
||||
In an advanced case, this function would allow an interceptor could add new
|
||||
In an advanced case, this function could allow an interceptor to add new
|
||||
interceptors to the `:queue` of a context."
|
||||
[context interceptors]
|
||||
(update context :queue
|
||||
|
@ -179,7 +179,7 @@
|
|||
The first few interceptors in a chain will likely have `:before`
|
||||
functions which \"prime\" the `context` by adding the event, and
|
||||
the current state of app-db into `:coeffects`. But interceptors can
|
||||
add whatever they want to `:coeffect` - perhaps the event handler needs
|
||||
add whatever they want to `:coeffects` - perhaps the event handler needs
|
||||
some information from localstore, or a random number, or access to
|
||||
a DataScript connection.
|
||||
|
||||
|
|
|
@ -71,3 +71,13 @@
|
|||
there isn't often much point firing a timed event in a test."
|
||||
[f ms]
|
||||
(next-tick f))
|
||||
|
||||
(defn now []
|
||||
;; currentTimeMillis may count backwards in some scenarios, but as this is used for tracing
|
||||
;; it is preferable to the slower but more accurate System.nanoTime.
|
||||
(System/currentTimeMillis))
|
||||
|
||||
(defn reagent-id
|
||||
"Doesn't make sense in a Clojure context currently."
|
||||
[reactive-val]
|
||||
nil)
|
||||
|
|
|
@ -36,3 +36,21 @@
|
|||
|
||||
(defn set-timeout! [f ms]
|
||||
(js/setTimeout f ms))
|
||||
|
||||
(defn now []
|
||||
(if (exists? js/performance.now)
|
||||
(js/performance.now)
|
||||
(js/Date.now)))
|
||||
|
||||
(defn reagent-id
|
||||
"Produces an id for reactive Reagent values
|
||||
e.g. reactions, ratoms, cursors."
|
||||
[reactive-val]
|
||||
(when (implements? reagent.ratom/IReactiveAtom reactive-val)
|
||||
(str (condp instance? reactive-val
|
||||
reagent.ratom/RAtom "ra"
|
||||
reagent.ratom/RCursor "rc"
|
||||
reagent.ratom/Reaction "rx"
|
||||
reagent.ratom/Track "tr"
|
||||
"other")
|
||||
(hash reactive-val))))
|
||||
|
|
|
@ -44,3 +44,8 @@
|
|||
[new-loggers]
|
||||
(assert (empty? (difference (set (keys new-loggers)) (-> @loggers keys set))) "Unknown keys in new-loggers")
|
||||
(swap! loggers merge new-loggers))
|
||||
|
||||
(defn get-loggers
|
||||
"Get the current logging functions used by re-frame."
|
||||
[]
|
||||
@loggers)
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
(ns re-frame.router
|
||||
(:require [re-frame.events :refer [handle]]
|
||||
[re-frame.interop :refer [after-render empty-queue next-tick]]
|
||||
[re-frame.loggers :refer [console]]))
|
||||
[re-frame.loggers :refer [console]]
|
||||
[re-frame.trace :as trace :include-macros true]))
|
||||
|
||||
|
||||
;; -- Router Loop ------------------------------------------------------------
|
||||
|
@ -85,7 +86,7 @@
|
|||
(-exception [this ex])
|
||||
(-pause [this later-fn])
|
||||
(-resume [this])
|
||||
(-call-post-event-callbacks[this event]))
|
||||
(-call-post-event-callbacks [this event]))
|
||||
|
||||
|
||||
;; Concrete implementation of IEventQueue
|
||||
|
@ -122,41 +123,46 @@
|
|||
;; Given a "trigger", and the existing FSM state, it computes the
|
||||
;; new FSM state and the transition action (function).
|
||||
|
||||
(let [[new-fsm-state action-fn]
|
||||
(case [fsm-state trigger]
|
||||
(trace/with-trace {:op-type ::fsm-trigger}
|
||||
(let [[new-fsm-state action-fn]
|
||||
(case [fsm-state trigger]
|
||||
|
||||
;; You should read the following "case" as:
|
||||
;; [current-FSM-state trigger] -> [new-FSM-state action-fn]
|
||||
;;
|
||||
;; So, for example, the next line should be interpreted as:
|
||||
;; if you are in state ":idle" and a trigger ":add-event"
|
||||
;; happens, then move the FSM to state ":scheduled" and execute
|
||||
;; that two-part "do" fucntion.
|
||||
[:idle :add-event] [:scheduled #(do (-add-event this arg)
|
||||
(-run-next-tick this))]
|
||||
;; You should read the following "case" as:
|
||||
;; [current-FSM-state trigger] -> [new-FSM-state action-fn]
|
||||
;;
|
||||
;; So, for example, the next line should be interpreted as:
|
||||
;; if you are in state ":idle" and a trigger ":add-event"
|
||||
;; happens, then move the FSM to state ":scheduled" and execute
|
||||
;; that two-part "do" function.
|
||||
[:idle :add-event] [:scheduled #(do (-add-event this arg)
|
||||
(-run-next-tick this))]
|
||||
|
||||
;; State: :scheduled (the queue is scheduled to run, soon)
|
||||
[:scheduled :add-event] [:scheduled #(-add-event this arg)]
|
||||
[:scheduled :run-queue] [:running #(-run-queue this)]
|
||||
;; State: :scheduled (the queue is scheduled to run, soon)
|
||||
[:scheduled :add-event] [:scheduled #(-add-event this arg)]
|
||||
[:scheduled :run-queue] [:running #(-run-queue this)]
|
||||
|
||||
;; State: :running (the queue is being processed one event after another)
|
||||
[:running :add-event ] [:running #(-add-event this arg)]
|
||||
[:running :pause ] [:paused #(-pause this arg)]
|
||||
[:running :exception ] [:idle #(-exception this arg)]
|
||||
[:running :finish-run] (if (empty? queue) ;; FSM guard
|
||||
[:idle]
|
||||
[:scheduled #(-run-next-tick this)])
|
||||
;; State: :running (the queue is being processed one event after another)
|
||||
[:running :add-event] [:running #(-add-event this arg)]
|
||||
[:running :pause] [:paused #(-pause this arg)]
|
||||
[:running :exception] [:idle #(-exception this arg)]
|
||||
[:running :finish-run] (if (empty? queue) ;; FSM guard
|
||||
[:idle]
|
||||
[:scheduled #(-run-next-tick this)])
|
||||
|
||||
;; State: :paused (:flush-dom metadata on an event has caused a temporary pause in processing)
|
||||
[:paused :add-event] [:paused #(-add-event this arg)]
|
||||
[:paused :resume ] [:running #(-resume this)]
|
||||
;; State: :paused (:flush-dom metadata on an event has caused a temporary pause in processing)
|
||||
[:paused :add-event] [:paused #(-add-event this arg)]
|
||||
[:paused :resume] [:running #(-resume this)]
|
||||
|
||||
(throw (ex-info (str "re-frame: router state transition not found. " fsm-state " " trigger)
|
||||
{:fsm-state fsm-state, :trigger trigger})))]
|
||||
(throw (ex-info (str "re-frame: router state transition not found. " fsm-state " " trigger)
|
||||
{:fsm-state fsm-state, :trigger trigger})))]
|
||||
|
||||
;; The "case" above computed both the new FSM state, and the action. Now, make it happen.
|
||||
(set! fsm-state new-fsm-state)
|
||||
(when action-fn (action-fn))))
|
||||
;; The "case" above computed both the new FSM state, and the action. Now, make it happen.
|
||||
|
||||
(trace/merge-trace! {:operation [fsm-state trigger]
|
||||
:tags {:current-state fsm-state
|
||||
:new-state new-fsm-state}})
|
||||
(set! fsm-state new-fsm-state)
|
||||
(when action-fn (action-fn)))))
|
||||
|
||||
(-add-event
|
||||
[_ event]
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
[re-frame.registrar :as registrar]
|
||||
[re-frame.db :refer [app-db]]
|
||||
[clojure.data :as data]
|
||||
[re-frame.cofx :as cofx]))
|
||||
[re-frame.cofx :as cofx]
|
||||
[re-frame.utils :as utils]))
|
||||
|
||||
|
||||
;; XXX provide a way to set what handler should be called when there is no registered handler.
|
||||
|
@ -63,7 +64,14 @@
|
|||
:id :trim-v
|
||||
:before (fn trimv-before
|
||||
[context]
|
||||
(update-in context [:coeffects :event] subvec 1))))
|
||||
(-> context
|
||||
(update-in [:coeffects :event] subvec 1)
|
||||
(assoc-in [:coeffects ::untrimmed-event] (get-coeffect context :event))))
|
||||
:after (fn trimv-after
|
||||
[context]
|
||||
(-> context
|
||||
(utils/dissoc-in [:coeffects ::untrimmed-event])
|
||||
(assoc-in [:coeffects :event] (get-coeffect context ::untrimmed-event))))))
|
||||
|
||||
|
||||
;; -- Interceptor Factories - PART 1 ---------------------------------------------------------------
|
||||
|
@ -133,7 +141,7 @@
|
|||
|
||||
(defn path
|
||||
"An interceptor factory which supplies a sub-path of `:db` to the handler.
|
||||
It's action is somewhat annologous to `update-in`. It grafts the return
|
||||
It's action is somewhat analogous to `update-in`. It grafts the return
|
||||
value from the handler back into db.
|
||||
|
||||
Usage:
|
||||
|
@ -144,7 +152,7 @@
|
|||
|
||||
Notes:
|
||||
1. cater for `path` appearing more than once in an interceptor chain.
|
||||
2. `:effect` may not contain `:db` effect. Which means no change to
|
||||
2. `:effects` may not contain `:db` effect. Which means no change to
|
||||
`:db` should be made.
|
||||
"
|
||||
[& args]
|
||||
|
@ -208,7 +216,9 @@
|
|||
:after (fn enrich-after
|
||||
[context]
|
||||
(let [event (get-coeffect context :event)
|
||||
db (get-effect context :db)]
|
||||
db (or (get-effect context :db)
|
||||
;; If no db effect is returned, we provide the original coeffect.
|
||||
(get-coeffect context :db))]
|
||||
(->> (f db event)
|
||||
(assoc-effect context :db))))))
|
||||
|
||||
|
@ -218,8 +228,9 @@
|
|||
"Interceptor factory which runs a given function `f` in the \"after\"
|
||||
position, presumably for side effects.
|
||||
|
||||
`f` is called with two arguments: the `effects` value of `:db` and the event. It's return
|
||||
value is ignored so `f` can only side-effect.
|
||||
`f` is called with two arguments: the `effects` value of `:db`
|
||||
(or the `coeffect` value of db if no db effect is returned) and the event.
|
||||
Its return value is ignored so `f` can only side-effect.
|
||||
|
||||
Example use:
|
||||
- `f` runs schema validation (reporting any errors found)
|
||||
|
@ -229,7 +240,9 @@
|
|||
:id :after
|
||||
:after (fn after-after
|
||||
[context]
|
||||
(let [db (get-effect context :db)
|
||||
(let [db (or (get-effect context :db)
|
||||
;; If no db effect is returned, we provide the original coeffect.
|
||||
(get-coeffect context :db))
|
||||
event (get-coeffect context :event)]
|
||||
(f db event) ;; call f for side effects
|
||||
context)))) ;; context is unchanged
|
||||
|
@ -274,5 +287,3 @@
|
|||
(assoc-in new-db out-path)
|
||||
(assoc-effect context :db))
|
||||
context)))))
|
||||
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
(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.interop :refer [add-on-dispose! debug-enabled? make-reaction ratom? deref? dispose! reagent-id]]
|
||||
[re-frame.loggers :refer [console]]
|
||||
[re-frame.utils :refer [first-in-vector]]
|
||||
[re-frame.registrar :refer [get-handler clear-handlers register-handler]]))
|
||||
|
||||
[re-frame.registrar :refer [get-handler clear-handlers register-handler]]
|
||||
[re-frame.trace :as trace :include-macros true]))
|
||||
|
||||
(def kind :sub)
|
||||
(assert (re-frame.registrar/kinds kind))
|
||||
|
@ -17,11 +17,29 @@
|
|||
;; Two subscriptions are "equal" if their query vectors test "=".
|
||||
(def query->reaction (atom {}))
|
||||
|
||||
(defn clear-subscription-cache!
|
||||
"Runs on-dispose for all subscriptions we have in the subscription cache.
|
||||
Used to force recreation of new subscriptions. Should only be necessary
|
||||
in development.
|
||||
|
||||
The on-dispose functions for the subscriptions will remove themselves from the
|
||||
cache.
|
||||
|
||||
Useful when reloading Figwheel code after a React exception, as React components
|
||||
aren't cleaned up properly. This means a subscription's on-dispose function isn't
|
||||
run when the components are destroyed. If a bad subscription caused your exception,
|
||||
then you can't fix it without reloading your browser."
|
||||
[]
|
||||
(doseq [[k rxn] @query->reaction]
|
||||
(dispose! rxn))
|
||||
(if (not-empty @query->reaction)
|
||||
(console :warn "Subscription cache should be empty after clearing it.")))
|
||||
|
||||
(defn clear-all-handlers!
|
||||
"Unregisters all existing subscription handlers"
|
||||
[]
|
||||
(clear-handlers kind)
|
||||
(reset! query->reaction {}))
|
||||
(clear-subscription-cache!))
|
||||
|
||||
(defn cache-and-return
|
||||
"cache the reaction r"
|
||||
|
@ -29,9 +47,14 @@
|
|||
(let [cache-key [query-v dynv]]
|
||||
;; 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)))
|
||||
(trace/with-trace {:operation (first-in-vector query-v)
|
||||
:op-type :sub/dispose
|
||||
:tags {:query-v query-v
|
||||
:reaction (reagent-id r)}}
|
||||
nil)))
|
||||
;; cache this reaction, so it can be used to deduplicate other, later "=" subscriptions
|
||||
(swap! query->reaction assoc cache-key r)
|
||||
(trace/merge-trace! {:tags {:reaction (reagent-id r)}})
|
||||
r)) ;; return the actual reaction
|
||||
|
||||
(defn cache-lookup
|
||||
|
@ -46,33 +69,48 @@
|
|||
(defn subscribe
|
||||
"Returns a Reagent/reaction which contains a computation"
|
||||
([query-v]
|
||||
(trace/with-trace {:operation (first-in-vector query-v)
|
||||
:op-type :sub/create
|
||||
:tags {:query-v query-v}}
|
||||
(if-let [cached (cache-lookup query-v)]
|
||||
(do ;(console :log "Using cached subscription: " query-v)
|
||||
(do
|
||||
(trace/merge-trace! {:tags {:cached? true
|
||||
:reaction (reagent-id cached)}})
|
||||
cached)
|
||||
|
||||
(let [query-id (first-in-vector query-v)
|
||||
handler-fn (get-handler kind query-id)]
|
||||
;(console :log "Subscription created: " query-v)
|
||||
(if-not handler-fn
|
||||
(console :error (str "re-frame: no subscription handler registered for: \"" query-id "\". Returning a nil subscription.")))
|
||||
(cache-and-return query-v [] (handler-fn app-db query-v)))))
|
||||
(trace/merge-trace! {:tags {:cached? false}})
|
||||
(if (nil? handler-fn)
|
||||
(do (trace/merge-trace! {:error true})
|
||||
(console :error (str "re-frame: no subscription handler registered for: \"" query-id "\". Returning a nil subscription.")))
|
||||
(cache-and-return query-v [] (handler-fn app-db query-v)))))))
|
||||
|
||||
([v dynv]
|
||||
(trace/with-trace {:operation (first-in-vector v)
|
||||
:op-type :sub/create
|
||||
:tags {:query-v v
|
||||
:dyn-v dynv}}
|
||||
(if-let [cached (cache-lookup v dynv)]
|
||||
(do ;(console :log "Using cached subscription: " v " and " dynv)
|
||||
(do
|
||||
(trace/merge-trace! {:tags {:cached? true
|
||||
:reaction (reagent-id cached)}})
|
||||
cached)
|
||||
(let [query-id (first-in-vector v)
|
||||
handler-fn (get-handler kind query-id)]
|
||||
(trace/merge-trace! {:tags {:cached? false}})
|
||||
(when debug-enabled?
|
||||
(when-let [not-reactive (not-empty (remove ratom? dynv))]
|
||||
(console :warn "re-frame: your subscription's dynamic parameters that don't implement IReactiveAtom:" not-reactive)))
|
||||
(if (nil? handler-fn)
|
||||
(when-not handler-fn
|
||||
(trace/merge-trace! {:error true})
|
||||
(console :error (str "re-frame: no subscription handler registered for: \"" query-id "\". Returning a nil subscription."))
|
||||
(let [dyn-vals (make-reaction (fn [] (mapv deref dynv)))
|
||||
sub (make-reaction (fn [] (handler-fn app-db v @dyn-vals)))]
|
||||
sub (make-reaction (fn [] (handler-fn app-db v @dyn-vals)))]
|
||||
;; handler-fn returns a reaction which is then wrapped in the sub reaction
|
||||
;; need to double deref it to get to the actual value.
|
||||
;(console :log "Subscription created: " v dynv)
|
||||
(cache-and-return v dynv (make-reaction (fn [] @@sub)))))))))
|
||||
(cache-and-return v dynv (make-reaction (fn [] @@sub))))))))))
|
||||
|
||||
;; -- reg-sub -----------------------------------------------------------------
|
||||
|
||||
|
@ -87,11 +125,13 @@
|
|||
|
||||
(defn- deref-input-signals
|
||||
[signals query-id]
|
||||
(cond
|
||||
(let [signals (cond
|
||||
(sequential? signals) (map deref signals)
|
||||
(map? signals) (map-vals deref signals)
|
||||
(deref? signals) @signals
|
||||
:else (console :error "re-frame: in the reg-sub for " query-id ", the input-signals function returns: " signals)))
|
||||
(map? signals) (map-vals deref signals)
|
||||
(deref? signals) @signals
|
||||
:else (console :error "re-frame: in the reg-sub for " query-id ", the input-signals function returns: " signals))]
|
||||
(trace/merge-trace! {:tags {:input-signals (map reagent-id signals)}})
|
||||
signals))
|
||||
|
||||
|
||||
(defn reg-sub
|
||||
|
@ -143,29 +183,43 @@
|
|||
f)
|
||||
|
||||
;; one sugar pair
|
||||
2 (let [ret-val (subscribe (second input-args))]
|
||||
(fn inp-fn
|
||||
([_] ret-val)
|
||||
([_ _] ret-val)))
|
||||
2 (fn inp-fn
|
||||
([_] (subscribe (second input-args)))
|
||||
([_ _] (subscribe (second input-args))))
|
||||
|
||||
;; multiple sugar pairs
|
||||
(let [pairs (partition 2 input-args)
|
||||
vecs (map last pairs)
|
||||
ret-val (map subscribe vecs)]
|
||||
vecs (map last pairs)]
|
||||
(when-not (every? vector? vecs)
|
||||
(console :error err-header "expected pairs of :<- and vectors, got:" pairs))
|
||||
(fn inp-fn
|
||||
([_] ret-val)
|
||||
([_ _] ret-val))))]
|
||||
([_] (map subscribe vecs))
|
||||
([_ _] (map subscribe vecs)))))]
|
||||
(register-handler
|
||||
kind
|
||||
query-id
|
||||
(fn subs-handler-fn
|
||||
([db query-vec]
|
||||
(let [subscriptions (inputs-fn query-vec)]
|
||||
(make-reaction
|
||||
(fn [] (computation-fn (deref-input-signals subscriptions query-id) query-vec)))))
|
||||
(let [subscriptions (inputs-fn query-vec)
|
||||
reaction-id (atom nil)
|
||||
reaction (make-reaction
|
||||
(fn [] (trace/with-trace {:operation (first-in-vector query-vec)
|
||||
:op-type :sub/run
|
||||
:tags {:query-v query-vec
|
||||
:reaction @reaction-id}}
|
||||
(computation-fn (deref-input-signals subscriptions query-id) query-vec))))]
|
||||
(reset! reaction-id (reagent-id reaction))
|
||||
reaction))
|
||||
([db query-vec dyn-vec]
|
||||
(let [subscriptions (inputs-fn query-vec dyn-vec)]
|
||||
(make-reaction
|
||||
(fn [] (computation-fn (deref-input-signals subscriptions query-id) query-vec dyn-vec)))))))))
|
||||
(let [subscriptions (inputs-fn query-vec dyn-vec)
|
||||
reaction-id (atom nil)
|
||||
reaction (make-reaction
|
||||
(fn []
|
||||
(trace/with-trace {:operation (first-in-vector query-vec)
|
||||
:op-type :sub/run
|
||||
:tags {:query-v query-vec
|
||||
:dyn-v dyn-vec
|
||||
:reaction @reaction-id}}
|
||||
(computation-fn (deref-input-signals subscriptions query-id) query-vec dyn-vec))))]
|
||||
(reset! reaction-id (reagent-id reaction))
|
||||
reaction))))))
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
(ns re-frame.trace
|
||||
"Tracing for re-frame.
|
||||
Alpha quality, subject to change/break at any time."
|
||||
(:require [re-frame.interop :as interop]
|
||||
[re-frame.loggers :refer [console]]))
|
||||
|
||||
(def id (atom 0))
|
||||
(def ^:dynamic *current-trace* nil)
|
||||
|
||||
(defn reset-tracing! []
|
||||
(reset! id 0))
|
||||
|
||||
#?(:cljs (goog-define trace-enabled? false)
|
||||
:clj (def ^boolean trace-enabled? false))
|
||||
|
||||
(defn ^boolean is-trace-enabled?
|
||||
"See https://groups.google.com/d/msg/clojurescript/jk43kmYiMhA/IHglVr_TPdgJ for more details"
|
||||
[]
|
||||
trace-enabled?)
|
||||
|
||||
(def trace-cbs (atom {}))
|
||||
|
||||
(defn register-trace-cb
|
||||
"Registers a tracing callback function which will receive a collection of one or more traces.
|
||||
Will replace an existing callback function if it shares the same key."
|
||||
[key f]
|
||||
(swap! trace-cbs assoc key f))
|
||||
|
||||
(defn remove-trace-cb [key]
|
||||
(swap! trace-cbs dissoc key)
|
||||
nil)
|
||||
|
||||
(defn next-id [] (swap! id inc))
|
||||
|
||||
(defn start-trace [{:keys [operation op-type tags child-of]}]
|
||||
{:id (next-id)
|
||||
:operation operation
|
||||
:op-type op-type
|
||||
:tags tags
|
||||
:child-of (or child-of (:id *current-trace*))
|
||||
:start (interop/now)})
|
||||
|
||||
#?(:clj (defmacro finish-trace [trace]
|
||||
`(when (is-trace-enabled?)
|
||||
(let [end# (interop/now)
|
||||
duration# (- end# (:start ~trace))]
|
||||
(doseq [[k# cb#] @trace-cbs]
|
||||
(try (cb# [(assoc ~trace
|
||||
:duration duration#
|
||||
:end (interop/now))])
|
||||
#?(:clj (catch Exception e#
|
||||
(console :error "Error thrown from trace cb" k# "while storing" ~trace e#)))
|
||||
#?(:cljs (catch :default e#
|
||||
(console :error "Error thrown from trace cb" k# "while storing" ~trace e#)))))))))
|
||||
|
||||
#?(:clj (defmacro with-trace
|
||||
"Create a trace inside the scope of the with-trace macro
|
||||
|
||||
Common keys for trace-opts
|
||||
:op-type - what kind of operation is this? e.g. :sub/create, :render.
|
||||
:operation - identifier for the operation, for an subscription it would be the subscription keyword
|
||||
tags - a map of arbitrary kv pairs"
|
||||
[{:keys [operation op-type tags child-of] :as trace-opts} & body]
|
||||
`(if (is-trace-enabled?)
|
||||
(binding [*current-trace* (start-trace ~trace-opts)]
|
||||
(try ~@body
|
||||
(finally (finish-trace *current-trace*))))
|
||||
(do ~@body))))
|
||||
|
||||
#?(:clj (defmacro merge-trace! [m]
|
||||
;; Overwrite keys in tags, and all top level keys.
|
||||
`(when (is-trace-enabled?)
|
||||
(let [new-trace# (-> (update *current-trace* :tags merge (:tags ~m))
|
||||
(merge (dissoc ~m :tags)))]
|
||||
(set! *current-trace* new-trace#))
|
||||
nil)))
|
|
@ -2,10 +2,23 @@
|
|||
(:require
|
||||
[re-frame.loggers :refer [console]]))
|
||||
|
||||
(defn dissoc-in
|
||||
"Dissociates an entry from a nested associative structure returning a new
|
||||
nested structure. keys is a sequence of keys. Any empty maps that result
|
||||
will not be present in the new structure.
|
||||
The key thing is that 'm' remains identical? to istelf if the path was never present"
|
||||
[m [k & ks :as keys]]
|
||||
(if ks
|
||||
(if-let [nextmap (get m k)]
|
||||
(let [newmap (dissoc-in nextmap ks)]
|
||||
(if (seq newmap)
|
||||
(assoc m k newmap)
|
||||
(dissoc m k)))
|
||||
m)
|
||||
(dissoc m k)))
|
||||
|
||||
(defn first-in-vector
|
||||
[v]
|
||||
(if (vector? v)
|
||||
(first v)
|
||||
(console :error "re-frame: expected a vector, but got:" v)))
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
[cljs.test :refer-macros [is deftest async use-fixtures]]
|
||||
[re-frame.core :as re-frame]
|
||||
[re-frame.fx]
|
||||
[re-frame.interop :refer [set-timeout!]]))
|
||||
[re-frame.interop :refer [set-timeout!]]
|
||||
[re-frame.loggers :as log]
|
||||
[clojure.string :as str]))
|
||||
|
||||
;; ---- FIXTURES ---------------------------------------------------------------
|
||||
|
||||
|
@ -43,3 +45,20 @@
|
|||
1000)
|
||||
;; kick off main handler
|
||||
(re-frame/dispatch [::later-test]))))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::missing-handler-test
|
||||
(fn [_world _event-v]
|
||||
{:fx-not-exist [:nothing :here]}))
|
||||
|
||||
(deftest report-missing-handler
|
||||
(let [logs (atom [])
|
||||
log-fn (fn [& args] (swap! logs conj (str/join args)))
|
||||
original-loggers (log/get-loggers)]
|
||||
(try
|
||||
(log/set-loggers! {:error log-fn})
|
||||
(re-frame/dispatch-sync [::missing-handler-test])
|
||||
(is (re-matches #"re-frame: no :fx handler registered for::fx-not-exist" (first @logs)))
|
||||
(is (= (count @logs) 1))
|
||||
(finally
|
||||
(log/set-loggers! original-loggers)))))
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
(ns re-frame.interceptor-test
|
||||
(:require [cljs.test :refer-macros [is deftest]]
|
||||
[reagent.ratom :refer [atom]]
|
||||
(:require [cljs.test :refer-macros [is deftest testing]]
|
||||
[reagent.ratom :refer [atom]]
|
||||
[re-frame.interceptor :refer [context get-coeffect assoc-effect assoc-coeffect get-effect]]
|
||||
[re-frame.std-interceptors :refer [trim-v path on-changes
|
||||
db-handler->interceptor fx-handler->interceptor]]))
|
||||
[re-frame.std-interceptors :refer [debug trim-v path enrich after on-changes
|
||||
db-handler->interceptor fx-handler->interceptor]]
|
||||
[re-frame.interceptor :as interceptor]))
|
||||
|
||||
(enable-console-print!)
|
||||
|
||||
(deftest test-trim-v
|
||||
(let [c (-> (context [:a :b :c] [])
|
||||
((:before trim-v)))]
|
||||
(is (= (get-coeffect c :event)
|
||||
[:b :c]))))
|
||||
(let [ctx (context [:event-id :b :c] [])
|
||||
ctx-trimmed ((:before trim-v) ctx)
|
||||
ctx-untrimmed ((:after trim-v) ctx-trimmed)]
|
||||
(is (= (get-coeffect ctx-trimmed :event)
|
||||
[:b :c]))
|
||||
(is (= (get-coeffect ctx-untrimmed :event)
|
||||
[:event-id :b :c]))
|
||||
(is (= ctx-untrimmed ctx))))
|
||||
|
||||
|
||||
(deftest test-one-level-path
|
||||
|
@ -47,13 +52,22 @@
|
|||
((:after p))
|
||||
(get-effect :db))))
|
||||
|
||||
;; test #2 - set dbto nil
|
||||
;; test #2 - set db to nil
|
||||
(is (= {:1 {:2 nil}}
|
||||
(-> b4
|
||||
(assoc-effect :db nil) ;; <-- db becomes nil
|
||||
((:after p))
|
||||
(get-effect :db)))))))
|
||||
|
||||
(deftest path-with-no-db-returned
|
||||
(let [path-interceptor (path :a)]
|
||||
(-> (context [] [path-interceptor] {:a 1})
|
||||
(interceptor/invoke-interceptors :before)
|
||||
interceptor/change-direction
|
||||
(interceptor/invoke-interceptors :after)
|
||||
(get-effect :db)
|
||||
(nil?) ;; We don't expect an effect to be added.
|
||||
(is))))
|
||||
|
||||
(deftest test-db-handler-interceptor
|
||||
(let [event [:a :b]
|
||||
|
@ -115,5 +129,19 @@
|
|||
((:after change-i))
|
||||
(get-effect :db))))))
|
||||
|
||||
(deftest test-after
|
||||
(testing "when no db effect is returned"
|
||||
(let [after-db-val (atom nil)]
|
||||
(-> (context [:a :b]
|
||||
[(after (fn [db] (reset! after-db-val db)))]
|
||||
{:a 1})
|
||||
(interceptor/invoke-interceptors :before)
|
||||
interceptor/change-direction
|
||||
(interceptor/invoke-interceptors :after))
|
||||
(is (= @after-db-val {:a 1})))))
|
||||
|
||||
|
||||
(deftest test-enrich
|
||||
(testing "when no db effect is returned"
|
||||
(let [ctx (context [] [] {:a 1})]
|
||||
(is (= ::not-found (get-effect ctx :db ::not-found)))
|
||||
(-> ctx (:after (enrich (fn [db] (is (= db {:a 1})))))))))
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
(ns re-frame.restore-test
|
||||
(:require [cljs.test :refer-macros [is deftest async use-fixtures testing]]
|
||||
[re-frame.core :refer [make-restore-fn reg-sub subscribe]]
|
||||
[re-frame.subs :as subs]))
|
||||
|
||||
;; TODO: future tests in this area could check DB state and registrations are being correctly restored.
|
||||
|
||||
(use-fixtures :each {:before subs/clear-all-handlers!})
|
||||
|
||||
(defn one? [x] (= 1 x))
|
||||
(defn two? [x] (= 2 x))
|
||||
|
||||
(defn register-test-subs []
|
||||
(reg-sub
|
||||
:test-sub
|
||||
(fn [db ev]
|
||||
(:test-sub db)))
|
||||
|
||||
(reg-sub
|
||||
:test-sub2
|
||||
(fn [db ev]
|
||||
(:test-sub2 db))))
|
||||
|
||||
(deftest make-restore-fn-test
|
||||
(testing "no existing subs, then making one subscription"
|
||||
(register-test-subs)
|
||||
(let [original-subs @subs/query->reaction
|
||||
restore-fn (make-restore-fn)]
|
||||
(is (zero? (count original-subs)))
|
||||
@(subscribe [:test-sub])
|
||||
(is (one? (count @subs/query->reaction)))
|
||||
(is (contains? @subs/query->reaction [[:test-sub] []]))
|
||||
(restore-fn)
|
||||
(is (zero? (count @subs/query->reaction))))))
|
||||
|
||||
(deftest make-restore-fn-test2
|
||||
(testing "existing subs, making more subscriptions"
|
||||
(register-test-subs)
|
||||
@(subscribe [:test-sub])
|
||||
(let [original-subs @subs/query->reaction
|
||||
restore-fn (make-restore-fn)]
|
||||
(is (one? (count original-subs)))
|
||||
@(subscribe [:test-sub2])
|
||||
(is (contains? @subs/query->reaction [[:test-sub2] []]))
|
||||
(is (two? (count @subs/query->reaction)))
|
||||
(restore-fn)
|
||||
(is (not (contains? @subs/query->reaction [[:test-sub2] []])))
|
||||
(is (one? (count @subs/query->reaction))))))
|
||||
|
||||
(deftest make-restore-fn-test3
|
||||
(testing "existing subs, making more subscriptions with different params on same subscriptions"
|
||||
(register-test-subs)
|
||||
@(subscribe [:test-sub])
|
||||
(let [original-subs @subs/query->reaction
|
||||
restore-fn (make-restore-fn)]
|
||||
(is (one? (count original-subs)))
|
||||
@(subscribe [:test-sub :extra :params])
|
||||
(is (two? (count @subs/query->reaction)))
|
||||
(restore-fn)
|
||||
(is (one? (count @subs/query->reaction))))))
|
||||
|
||||
(deftest nested-restores
|
||||
(testing "running nested restores"
|
||||
(register-test-subs)
|
||||
(let [restore-fn-1 (make-restore-fn)
|
||||
_ @(subscribe [:test-sub])
|
||||
_ (is (one? (count @subs/query->reaction)))
|
||||
restore-fn-2 (make-restore-fn)]
|
||||
@(subscribe [:test-sub2])
|
||||
(is (two? (count @subs/query->reaction)))
|
||||
(restore-fn-2)
|
||||
(is (one? (count @subs/query->reaction)))
|
||||
(restore-fn-1)
|
||||
(is (zero? (count @subs/query->reaction))))))
|
|
@ -1,15 +1,15 @@
|
|||
(ns re-frame.subs-test
|
||||
(:require [cljs.test :refer-macros [is deftest]]
|
||||
(:require [cljs.test :as test :refer-macros [is deftest]]
|
||||
[reagent.ratom :refer-macros [reaction]]
|
||||
[re-frame.subs :as subs]
|
||||
[re-frame.db :as db]
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
(test/use-fixtures :each {:before (fn [] (subs/clear-all-handlers!))})
|
||||
|
||||
;=====test basic subscriptions ======
|
||||
|
||||
(deftest test-reg-sub
|
||||
(subs/clear-all-handlers!)
|
||||
|
||||
(re-frame/reg-sub-raw
|
||||
:test-sub
|
||||
(fn [db [_]] (reaction (deref db))))
|
||||
|
@ -20,8 +20,6 @@
|
|||
(is (= 1 @test-sub))))
|
||||
|
||||
(deftest test-chained-subs
|
||||
(subs/clear-all-handlers!)
|
||||
|
||||
(re-frame/reg-sub-raw
|
||||
:a-sub
|
||||
(fn [db [_]] (reaction (:a @db))))
|
||||
|
@ -44,8 +42,6 @@
|
|||
(is (= {:a 1 :b 3} @test-sub))))
|
||||
|
||||
(deftest test-sub-parameters
|
||||
(subs/clear-all-handlers!)
|
||||
|
||||
(re-frame/reg-sub-raw
|
||||
:test-sub
|
||||
(fn [db [_ b]] (reaction [(:a @db) b])))
|
||||
|
@ -56,8 +52,6 @@
|
|||
|
||||
|
||||
(deftest test-sub-chained-parameters
|
||||
(subs/clear-all-handlers!)
|
||||
|
||||
(re-frame/reg-sub-raw
|
||||
:a-sub
|
||||
(fn [db [_ a]] (reaction [(:a @db) a])))
|
||||
|
@ -77,12 +71,13 @@
|
|||
(reset! db/app-db {:a 1 :b 2})
|
||||
(is (= {:a [1 :c], :b [2 :c]} @test-sub))))
|
||||
|
||||
(deftest test-nonexistent-sub
|
||||
(is (nil? (re-frame/subscribe [:non-existence]))))
|
||||
|
||||
;============== test cached-subs ================
|
||||
(def side-effect-atom (atom 0))
|
||||
|
||||
(deftest test-cached-subscriptions
|
||||
(subs/clear-all-handlers!)
|
||||
(reset! side-effect-atom 0)
|
||||
|
||||
(re-frame/reg-sub-raw
|
||||
|
@ -106,8 +101,6 @@
|
|||
;============== test register-pure macros ================
|
||||
|
||||
(deftest test-reg-sub-macro
|
||||
(subs/clear-all-handlers!)
|
||||
|
||||
(subs/reg-sub
|
||||
:test-sub
|
||||
(fn [db [_]] db))
|
||||
|
@ -118,8 +111,6 @@
|
|||
(is (= 1 @test-sub))))
|
||||
|
||||
(deftest test-reg-sub-macro-singleton
|
||||
(subs/clear-all-handlers!)
|
||||
|
||||
(subs/reg-sub
|
||||
:a-sub
|
||||
(fn [db [_]] (:a db)))
|
||||
|
@ -138,8 +129,6 @@
|
|||
(is (= {:a 1} @test-sub))))
|
||||
|
||||
(deftest test-reg-sub-macro-vector
|
||||
(subs/clear-all-handlers!)
|
||||
|
||||
(subs/reg-sub
|
||||
:a-sub
|
||||
(fn [db [_]] (:a db)))
|
||||
|
@ -163,8 +152,6 @@
|
|||
(is (= {:a 1 :b 3} @test-sub))))
|
||||
|
||||
(deftest test-reg-sub-macro-map
|
||||
(subs/clear-all-handlers!)
|
||||
|
||||
(subs/reg-sub
|
||||
:a-sub
|
||||
(fn [db [_]] (:a db)))
|
||||
|
@ -188,8 +175,6 @@
|
|||
(is (= {:a 1 :b 3} @test-sub))))
|
||||
|
||||
(deftest test-sub-macro-parameters
|
||||
(subs/clear-all-handlers!)
|
||||
|
||||
(subs/reg-sub
|
||||
:test-sub
|
||||
(fn [db [_ b]] [(:a db) b]))
|
||||
|
@ -199,8 +184,6 @@
|
|||
(is (= [1 :c] @test-sub))))
|
||||
|
||||
(deftest test-sub-macros-chained-parameters
|
||||
(subs/clear-all-handlers!)
|
||||
|
||||
(subs/reg-sub
|
||||
:a-sub
|
||||
(fn [db [_ a]] [(:a db) a]))
|
||||
|
@ -222,8 +205,6 @@
|
|||
|
||||
(deftest test-sub-macros-<-
|
||||
"test the syntactial sugar"
|
||||
(subs/clear-all-handlers!)
|
||||
|
||||
(subs/reg-sub
|
||||
:a-sub
|
||||
(fn [db [_]] (:a db)))
|
||||
|
@ -239,8 +220,6 @@
|
|||
|
||||
(deftest test-sub-macros-chained-parameters-<-
|
||||
"test the syntactial sugar"
|
||||
(subs/clear-all-handlers!)
|
||||
|
||||
(subs/reg-sub
|
||||
:a-sub
|
||||
(fn [db [_]] (:a db)))
|
||||
|
@ -258,3 +237,34 @@
|
|||
(let [test-sub (subs/subscribe [:a-b-sub :c])]
|
||||
(reset! db/app-db {:a 1 :b 2})
|
||||
(is (= {:a 1 :b 2} @test-sub) )))
|
||||
|
||||
(deftest test-registering-subs-doesnt-create-subscription
|
||||
(let [sub-called? (atom false)]
|
||||
(with-redefs [subs/subscribe (fn [& args] (reset! sub-called? true))]
|
||||
(subs/reg-sub
|
||||
:a-sub
|
||||
(fn [db [_]] (:a db)))
|
||||
|
||||
(subs/reg-sub
|
||||
:b-sub
|
||||
(fn [db [_]] (:b db)))
|
||||
|
||||
(subs/reg-sub
|
||||
:fn-sub
|
||||
(fn [[_ c] _]
|
||||
[(subs/subscribe [:a-sub c])
|
||||
(subs/subscribe [:b-sub c])])
|
||||
(fn [db [_]] (:b db)))
|
||||
|
||||
(subs/reg-sub
|
||||
:a-sugar-sub
|
||||
:<- [:a-sub]
|
||||
(fn [[a] [_ c]] {:a a}))
|
||||
|
||||
(subs/reg-sub
|
||||
:a-b-sub
|
||||
:<- [:a-sub]
|
||||
:<- [:b-sub]
|
||||
(fn [[a b] [_ c]] {:a a :b b})))
|
||||
|
||||
(is (false? @sub-called?))))
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
;; Test Namespaces -------------------------------
|
||||
[re-frame.interceptor-test]
|
||||
[re-frame.subs-test]
|
||||
[re-frame.fx-test]))
|
||||
[re-frame.fx-test]
|
||||
[re-frame.trace-test]
|
||||
[re-frame.restore-test]))
|
||||
|
||||
(enable-console-print!)
|
||||
|
||||
|
@ -19,7 +21,9 @@
|
|||
(cljs-test/run-tests
|
||||
're-frame.interceptor-test
|
||||
're-frame.subs-test
|
||||
're-frame.fx-test))
|
||||
're-frame.fx-test
|
||||
're-frame.trace-test
|
||||
're-frame.restore-test))
|
||||
|
||||
;; ---- KARMA -----------------------------------------------------------------
|
||||
|
||||
|
@ -28,4 +32,6 @@
|
|||
karma
|
||||
're-frame.interceptor-test
|
||||
're-frame.subs-test
|
||||
're-frame.fx-test))
|
||||
're-frame.fx-test
|
||||
're-frame.trace-test
|
||||
're-frame.restore-test))
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
(ns re-frame.trace-test
|
||||
(:require [cljs.test :as test :refer-macros [is deftest]]
|
||||
[re-frame.trace :as trace :include-macros true]
|
||||
[re-frame.core :as rf]))
|
||||
|
||||
(def test-traces (atom []))
|
||||
|
||||
(test/use-fixtures :once {:before (fn []
|
||||
(trace/register-trace-cb :test
|
||||
(fn [traces]
|
||||
(doseq [trace traces]
|
||||
(swap! test-traces conj trace)))))
|
||||
:after (fn []
|
||||
(trace/remove-trace-cb :test))})
|
||||
|
||||
(test/use-fixtures :each {:before (fn []
|
||||
(reset! test-traces [])
|
||||
(trace/reset-tracing!))})
|
||||
|
||||
; Disabled, as goog-define doesn't work in optimizations :whitespace
|
||||
;(deftest trace-cb-test
|
||||
; (trace/with-trace {:operation :test1
|
||||
; :op-type :test})
|
||||
; (is (= 1 (count @test-traces)))
|
||||
; (is (= (select-keys (first @test-traces) [:id :operation :op-type :tags])
|
||||
; {:id 1 :operation :test1 :op-type :test :tags nil})))
|
||||
;
|
||||
;(enable-console-print!)
|
||||
;
|
||||
;(deftest sub-trace-test
|
||||
; (rf/subscribe [:non-existence])
|
||||
; (is (= 1 (count @test-traces)))
|
||||
; (is (= (select-keys (first @test-traces) [:id :operation :op-type :error])
|
||||
; {:id 1 :op-type :sub/create :operation :non-existence :error true})))
|