mirror of
https://github.com/status-im/re-frame.git
synced 2025-02-22 14:58:12 +00:00
Merge branch 'master' into master
This commit is contained in:
commit
bcd852d089
14
CHANGES.md
14
CHANGES.md
@ -1,9 +1,19 @@
|
||||
## Unreleased
|
||||
|
||||
## Unreleased
|
||||
#### New
|
||||
|
||||
- improved [testing docs](https://github.com/Day8/re-frame/blob/master/docs/Testing.md)
|
||||
- added [a new mental model](https://github.com/Day8/re-frame/blob/master/docs/MentalModelOmnibus.md#dsls-and-vms)
|
||||
|
||||
#### Fixes
|
||||
|
||||
- [#357](https://github.com/Day8/re-frame/pull/357)
|
||||
- [#340](https://github.com/Day8/re-frame/pull/340)
|
||||
|
||||
## 0.9.4 (2017.06.01)
|
||||
|
||||
#### Improvements
|
||||
|
||||
- added a CITATION.md file
|
||||
- re-frame now supports self-hosted ClojureScript at an alpha/unofficial/experimental level. It may be removed in the future if it causes problems elsewhere. [#325](https://github.com/Day8/re-frame/pull/325)
|
||||
|
||||
## 0.9.3 (2017.05.15)
|
||||
|
15
CITATION.md
Normal file
15
CITATION.md
Normal file
@ -0,0 +1,15 @@
|
||||
To cite re-frame in publications, please use:
|
||||
|
||||
[](https://doi.org/10.5281/zenodo.801613)
|
||||
|
||||
Thompson, M. (2015, March). Re-Frame: A Reagent Framework For Writing SPAs, in Clojurescript.
|
||||
Zenodo. http://doi.org/10.5281/zenodo.801613
|
||||
|
||||
@misc{thompson_2015,
|
||||
author = {Thompson, Michael},
|
||||
title = {Re-Frame: A Reagent Framework For Writing SPAs, in Clojurescript.},
|
||||
month = mar,
|
||||
year = 2015,
|
||||
doi = {10.5281/zenodo.801613},
|
||||
url = {https://doi.org/10.5281/zenodo.801613}
|
||||
}
|
212
README.md
212
README.md
@ -18,6 +18,16 @@ y'know. Pretty good.
|
||||
[](https://circleci.com/gh/Day8/re-frame/tree/develop)
|
||||
[](https://circleci.com/gh/Day8/re-frame/tree/master)
|
||||
|
||||
## re-frame
|
||||
|
||||
re-frame is a pattern for writing [SPAs] in ClojureScript, using [Reagent].
|
||||
|
||||
McCoy might report "It's MVC, Jim, but not as we know it". And you would respond
|
||||
"McCoy, you trouble maker, why even mention an OO pattern?
|
||||
re-frame is a **functional framework**."
|
||||
|
||||
Being a functional framework, it is about data, and the functions
|
||||
which transform that data.
|
||||
|
||||
## Why Should You Care?
|
||||
|
||||
@ -36,10 +46,10 @@ Perhaps:
|
||||
In this space, re-frame is very old, hopefully in a Gandalf kind of way.
|
||||
First designed in Dec 2014, it even slightly pre-dates the official Elm Architecture,
|
||||
although thankfully we were influenced by early-Elm concepts like `foldp` and `lift`, as well as
|
||||
terrific Clojure projects like [Pedestal App], [Om] and [Hoplon]. Since then,
|
||||
Clojure projects like [Pedestal App], [Om] and [Hoplon]. Since then,
|
||||
re-frame has pioneered ideas like event handler middleware,
|
||||
coeffect accretion, and de-duplicated signal graphs.
|
||||
5. Which leads us to the most important point: **re-frame is impressively buzzword compliant**. It has reactivity,
|
||||
5. Which brings us to the most important point: **re-frame is impressively buzzword compliant**. It has reactivity,
|
||||
unidirectional data flow, pristinely pure functions,
|
||||
interceptors, coeffects, conveyor belts, statechart-friendliness (FSM)
|
||||
and claims an immaculate hammock conception. It also has a charming
|
||||
@ -80,18 +90,7 @@ order functions). Etc.
|
||||
**Data - that's the way we roll.**
|
||||
|
||||
|
||||
## re-frame
|
||||
|
||||
re-frame is a pattern for writing [SPAs] in ClojureScript, using [Reagent].
|
||||
|
||||
McCoy might report "It's MVC, Jim, but not as we know it". And you would respond
|
||||
"McCoy, you trouble maker, why even mention an OO pattern?
|
||||
re-frame is a **functional framework**."
|
||||
|
||||
Being a functional framework, it is about data, and the functions
|
||||
which transform that data.
|
||||
|
||||
### It is a loop
|
||||
## It is a loop
|
||||
|
||||
Architecturally, re-frame implements "a perpetual loop".
|
||||
|
||||
@ -130,11 +129,17 @@ you to understand re-frame, is **practically proof** it does physics.
|
||||
Computationally, each iteration of the loop involves a
|
||||
six domino cascade.
|
||||
|
||||
One domino triggers the next, which triggers the next, et cetera, until we are
|
||||
back at the beginning of the loop, whereupon the dominoes spring to attention
|
||||
One domino triggers the next, which triggers the next, et cetera, boom, boom, boom, until we are
|
||||
back at the beginning of the loop, and the dominoes spring to attention
|
||||
again, ready for the next iteration of the same cascade.
|
||||
|
||||
The six dominoes are ...
|
||||
The six dominoes are:
|
||||
1. Event dispatch
|
||||
2. Event handling
|
||||
3. Effect handling
|
||||
4. Query
|
||||
5. View
|
||||
6. DOM
|
||||
|
||||
### 1st Domino - Event Dispatch
|
||||
|
||||
@ -244,19 +249,7 @@ This is the step in which the hiccup-formatted
|
||||
"descriptions of required DOM", returned by the view functions of Domino 5, are made real.
|
||||
The browser DOM nodes are mutated.
|
||||
|
||||
|
||||
### A Cascade Of Simple Functions
|
||||
|
||||
**Each of the dominoes you write are simple, pure functions** which
|
||||
can be described, understood and
|
||||
tested independently. They take data, transform it and return new data.
|
||||
|
||||
The loop itself is very mechanical in operation.
|
||||
So, there's a regularity, simplicity and
|
||||
certainty to how a re-frame app goes about its business,
|
||||
which leads, in turn, to an ease in reasoning and debugging.
|
||||
|
||||
### Managing mutation
|
||||
## Managing mutation
|
||||
|
||||
The two sub-cascades 1-2-3 and 4-5-6 have a similar structure.
|
||||
|
||||
@ -267,12 +260,22 @@ the last domino which does the dirty work and realises these descriptions.
|
||||
In both cases, you don't need to worry yourself about this dirty work. re-frame looks
|
||||
after those dominoes.
|
||||
|
||||
## Code Fragments For The Dominos
|
||||
### A Cascade Of Simple Functions
|
||||
|
||||
**You'll (mostly) be writing pure functions** which
|
||||
can be described, understood and
|
||||
tested independently. They take data, transform it and return new data.
|
||||
|
||||
The loop itself is mechanical and predictable in operation.
|
||||
So, there's a regularity to how a re-frame app goes about its business,
|
||||
which leads, in turn, to an ease in reasoning and debugging.
|
||||
|
||||
## The Dominoes Again - With Code Fragments
|
||||
|
||||
<img align="right" src="/images/Readme/todolist.png?raw=true">
|
||||
|
||||
So that was the view of re-frame from 60,000 feet. We'll now shift to 30,000 feet
|
||||
and look again at each domino, but this time with code fragments.
|
||||
So that was the view of re-frame from 60,000 feet. We'll now shift down to 30,000 feet
|
||||
and look again at each domino, but this time with code fragments.
|
||||
|
||||
**Imagine:** we're working on a SPA which displays a list of items. You have
|
||||
just clicked the "delete" button next to the 3rd item in the list.
|
||||
@ -285,53 +288,69 @@ to completely grok the terse code presented below. We're still at 30,000 feet. D
|
||||
|
||||
### Code For Domino 1
|
||||
|
||||
The delete button for that 3rd item will have an `on-click` handler (function) which looks
|
||||
like this:
|
||||
The delete button for that 3rd item will be rendered by a ViewFunction which looks like this:
|
||||
```clj
|
||||
#(re-frame.core/dispatch [:delete-item 2486])
|
||||
(defn delete-button
|
||||
[item-id]
|
||||
[:div.garbage-bin
|
||||
:on-click #(re-frame.core/dispatch [:delete-item item-id])])
|
||||
```
|
||||
|
||||
`dispatch` emits an `event`.
|
||||
That `on-click` handler uses re-frame's `dispatch` to emit an `event`.
|
||||
|
||||
A re-frame `event` is a vector and, in this case,
|
||||
it has 2 elements: `[:delete-item 2486]`. The first element,
|
||||
`:delete-item`, is the kind of event. The rest is optional, further data about the
|
||||
`event` - in this case, my made-up id, `2486`, for the item to delete.
|
||||
it has 2 elements: `[:delete-item 2486]` (where `2486` in the made-up id for that 3rd item).
|
||||
|
||||
The first element of an event vector,
|
||||
`:delete-item`, is the kind of event. The rest is optional, useful data about the
|
||||
`event`.
|
||||
|
||||
Events express intent in a domain specific way.
|
||||
They are the language of your re-frame system.
|
||||
|
||||
### Code For Domino 2
|
||||
|
||||
An `event handler` (function), `h`, is called to
|
||||
An `event handler` (function), called say `h`, is called to
|
||||
compute the `effect` of the event `[:delete-item 2486]`.
|
||||
|
||||
Earlier, on program startup, `h` would have been
|
||||
registered for handling `:delete-item` `events` like this:
|
||||
On app startup, `re-frame.core/reg-event-fx` would have been used to
|
||||
register this `h` as the handler for `:delete-item` events, like this:
|
||||
```clj
|
||||
(re-frame.core/reg-event-fx ;; a part of the re-frame API
|
||||
:delete-item ;; the kind of event
|
||||
h) ;; the handler function for this kind of event
|
||||
h) ;; the handler function for this kind of event
|
||||
```
|
||||
|
||||
`h` is written to take two arguments:
|
||||
1. a `coeffects` map which contains the current state of the world (including app state)
|
||||
2. the `event`
|
||||
|
||||
`h` returns a map of `effects` - a description
|
||||
of how the world should be changed by the event.
|
||||
2. the `event` to handle
|
||||
|
||||
It is the job of `h` to compute how the world should be changed by the event, and
|
||||
it returns a map of `effects` - a description of the those changes.
|
||||
|
||||
Here's a sketch (we are at 30,000 feet):
|
||||
```clj
|
||||
(defn h
|
||||
[{:keys [db]} event] ;; args: db from coeffect, event
|
||||
(let [item-id (second event)] ;; extract id from event vector
|
||||
{:db (dissoc-in db [:items item-id])})) ;; effect is change db
|
||||
(defn h ;; choose a better name like delete-item
|
||||
[coeffects event] ;; args: db from coeffect, event
|
||||
(let [item-id (second event) ;; extract id from event vector
|
||||
db (:db coeffects) ;; extract the current application state
|
||||
{:db (dissoc-in db [:items item-id])})) ;; effect is change app state
|
||||
```
|
||||
|
||||
re-frame has ways (beyond us here) to inject necessary aspects
|
||||
re-frame has ways (described in later tutorials) to inject necessary aspects
|
||||
of the world into that first `coeffects` argument (map). Different
|
||||
event handlers need to know different things about the world
|
||||
in order to get their job done. But current "application state"
|
||||
is one aspect of the world which is invariably needed, and it is made
|
||||
available by default in the `:db` key.
|
||||
event handlers need different "things" to get their job done. But
|
||||
current "application state" is one aspect of the world which is
|
||||
invariably needed, and it is available by default in the `:db` key.
|
||||
|
||||
BTW, here is a more idiomatic rewrite of `h` which uses "destructuring" of the args:
|
||||
|
||||
```clj
|
||||
(defn h
|
||||
[{:keys [db]} [_ item-id]] ;; <--- new: obtain db and id directly
|
||||
{:db (dissoc-in db [:items item-id])}) ;; same as before
|
||||
```
|
||||
|
||||
|
||||
### Code For Domino 3
|
||||
|
||||
@ -339,7 +358,7 @@ An `effect handler` (function) actions the `effects` returned by `h`.
|
||||
|
||||
Here's what `h` returned:
|
||||
```clj
|
||||
{:db (dissoc-in db [:items item-id])}
|
||||
{:db (dissoc-in db [:items 2486])} ;; db is a map of some structure
|
||||
```
|
||||
Each key of the map identifies one kind
|
||||
of `effect`, and the value for that key supplies further details.
|
||||
@ -351,52 +370,62 @@ This update of "app state" is a mutative step, facilitated by re-frame
|
||||
which has a built-in `effects handler` for the `:db` effect.
|
||||
|
||||
Why the name `:db`? Well, re-frame sees "app state" as something of an in-memory
|
||||
database. More on that soon.
|
||||
database. More on this is a following tutorial.
|
||||
|
||||
Just to be clear, if `h` had returned:
|
||||
```clj
|
||||
{:wear {:pants "velour flares" :belt false}
|
||||
:tweet "Okay, yes, I am Satoshi. #coverblown"}
|
||||
```
|
||||
Then the two effects handlers registered for `:wear` and `:tweet` would
|
||||
be called in this domino to action those two effects. And, no, re-frame
|
||||
Then, the two effects handlers registered for `:wear` and `:tweet` would
|
||||
be called to action those two effects. And, no, re-frame
|
||||
does not supply standard effect handlers for either, so you would have had
|
||||
to have written them yourself (see how in a later tutorial).
|
||||
|
||||
### Code For Domino 4
|
||||
|
||||
Because an effect handler just updated "app state",
|
||||
a query (function) over this app state is called automatically (reactively),
|
||||
itself computing the list of items.
|
||||
Because an effect handler just mutated "application state",
|
||||
a query (function) over this app state is automatically called (reactively).
|
||||
|
||||
This query (function) computes "a materialised view" of the
|
||||
application state - a version of the application state which is useful to
|
||||
the next domino, 5.
|
||||
|
||||
Remember, we are now within the `v = f(s)` part of the flow, and this
|
||||
domino is about delivering the right
|
||||
data (s) to later domino functions (f) which compute DOM (v).
|
||||
|
||||
Now, in this particular case, the query function is pretty trivial.
|
||||
Because the items are stored in app state, there's not a lot
|
||||
to compute in this case. This
|
||||
query function acts more like an extractor or accessor:
|
||||
to compute and, instead, it acts more like an extractor or accessor,
|
||||
just plucking the list of items out of application state:
|
||||
```clj
|
||||
(defn query-fn
|
||||
[db _] ;; db is current app state
|
||||
[db v] ;; db is current app state, v the query vector
|
||||
(:items db)) ;; not much of a materialised view
|
||||
```
|
||||
|
||||
On program startup, such a `query-fn` must be associated with a `query-id`,
|
||||
(for reasons obvious in the next domino) like this:
|
||||
(so it can be used via `subscribe` in the next domino) using `re-frame.core/reg-sub`,
|
||||
like this:
|
||||
```clj
|
||||
(re-frame.core/reg-sub ;; part of the re-frame API
|
||||
:query-items ;; query id
|
||||
query-fn) ;; query fn
|
||||
```
|
||||
Which says "if you see a query (subscribe) for `:query-items`,
|
||||
Which says "if you see a `(subscribe [:query-items])`, then
|
||||
use `query-fn` to compute it".
|
||||
|
||||
### Code For Domino 5
|
||||
|
||||
Because the query function for `:query-items` just re-computed a new value,
|
||||
any view (function) which subscribes to `:query-items`
|
||||
is called automatically (reactively) to re-compute DOM.
|
||||
any view (function) which uses a `(subscribe [:query-items])`
|
||||
is called automatically (reactively) to re-compute new DOM.
|
||||
|
||||
View functions compute a data structure, in hiccup format, describing
|
||||
the DOM nodes required. In this case, there will be no DOM nodes
|
||||
for the now-deleted item, obviously, but otherwise the same DOM as last time.
|
||||
the DOM nodes required. In this case, the view functions will *not* be generating
|
||||
hiccup for the now-deleted item obviously but, other than this,
|
||||
the hiccup computed will be the same as last time.
|
||||
|
||||
```clj
|
||||
(defn items-view
|
||||
@ -405,8 +434,21 @@ for the now-deleted item, obviously, but otherwise the same DOM as last time.
|
||||
[:div (map item-render @items)])) ;; assume item-render already written
|
||||
```
|
||||
|
||||
Notice how `items` is "sourced" from "app state" via `subscribe`.
|
||||
It is called with a query id to identify what data it needs.
|
||||
Notice how `items` is "sourced" from "app state" via `re-frame.core/subscribe`.
|
||||
It is called with a vector argument, and the first element of that vector is
|
||||
a query-id which identifies the "materialised view" required.
|
||||
|
||||
Note: `subscribe` queries can be parameterised. So, in real world apps
|
||||
you might have this:<br>
|
||||
`(subscribe [:items "blue"])`
|
||||
|
||||
The vector identifies, first, the query, and then
|
||||
supplies further arguments. You could think of that as
|
||||
representing `select * from Items where colour="blue"`.
|
||||
|
||||
Except there's no SQL available and you would be the one to implement
|
||||
the more sophisticated `query-fn` capable of handling the
|
||||
"where" argument. More in later tutorials.
|
||||
|
||||
### Code For Domino 6
|
||||
|
||||
@ -444,7 +486,7 @@ When building a re-frame app, you:
|
||||
- write Reagent view functions (view layer) (domino 5)
|
||||
|
||||
|
||||
## It is mature and proven in the large
|
||||
## re-frame is mature and proven in the large
|
||||
|
||||
re-frame was released in early 2015, and has since
|
||||
[been](https://www.fullcontact.com) successfully
|
||||
@ -475,28 +517,16 @@ and useful 3rd party libraries.
|
||||
|
||||
## Where Do I Go Next?
|
||||
|
||||
**At this point you
|
||||
already know 50% of re-frame.** There's detail to fill in, for sure,
|
||||
but the core concepts, and even basic coding techniques, are now known to you.
|
||||
At this point, you know 50% of re-frame. The full [docs are here](/docs/README.md).
|
||||
|
||||
Next you need to read the other three articles in the [Introduction section](/docs#introduction):
|
||||
|
||||
* [Application State](/docs/ApplicationState.md)
|
||||
* [Code Walkthrough](/docs/CodeWalkthrough.md)
|
||||
* [Mental Model Omnibus](/docs/MentalModelOmnibus.md)
|
||||
|
||||
This will push your knowledge to about 70%. The
|
||||
final 30% will come incrementally with use, and by reading the other
|
||||
tutorials (of which there's a few).
|
||||
|
||||
You can also experiment with these two examples: <br>
|
||||
There are two example apps to play with: <br>
|
||||
https://github.com/Day8/re-frame/tree/master/examples
|
||||
|
||||
Use a template to create your own project: <br>
|
||||
Client only: https://github.com/Day8/re-frame-template <br>
|
||||
Full Stack: http://www.luminusweb.net/
|
||||
|
||||
Use these resources: <br>
|
||||
And please be sure to review these further resources: <br>
|
||||
https://github.com/Day8/re-frame/blob/develop/docs/External-Resources.md
|
||||
|
||||
### T-Shirt Unlocked
|
||||
|
@ -7,3 +7,5 @@ test:
|
||||
override:
|
||||
- lein karma-once
|
||||
- karma start --single-run --reporters junit,dots
|
||||
- lein cljsbuild once:
|
||||
pwd: examples/todomvc
|
||||
|
@ -30,7 +30,7 @@ Then:
|
||||
1. `git clone https://github.com/Day8/re-frame.git`
|
||||
2. `cd re-frame/examples/simple`
|
||||
3. `lein do clean, figwheel`
|
||||
4. wait a minute and then open `http://localhost:3449/example.html`
|
||||
4. wait a minute and then open <http://localhost:3449/example.html>
|
||||
|
||||
So, what's just happened? The ClojureScript code under `/src` has been compiled into `javascript` and
|
||||
put into `/resources/public/js/client.js` which is loaded into `/resources/public/example.html` (the HTML you just opened)
|
||||
|
@ -6,37 +6,24 @@ Please add to this list by submitting a pull request.
|
||||
### Templates
|
||||
|
||||
* [re-frame-template](https://github.com/Day8/re-frame-template) - Generates the client side SPA
|
||||
|
||||
* [Luminus](http://www.luminusweb.net) - Generates SPA plus server side.
|
||||
|
||||
* [re-natal](https://github.com/drapanjanas/re-natal) - React Native apps.
|
||||
|
||||
* [Slush-reframe](https://github.com/kristianmandrup/slush-reframe) - A scaffolding generator for re-frame run using NodeJS. Should work wih re-frame `0.7.0` if used on a project started from the `0.7.0` version of re-frame-template.
|
||||
|
||||
* [Slush-reframe](https://github.com/kristianmandrup/slush-reframe) - A scaffolding generator for re-frame run using NodeJS. Based on re-frame `0.7.0`
|
||||
* [Celibidache](https://github.com/velveteer/celibidache/) - An opinionated starter for re-frame applications using Boot. Based on re-frame `0.7.0`
|
||||
|
||||
|
||||
### 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) - Tutorial with links to code and live example.
|
||||
|
||||
* [Elfeed-cljsrn](https://github.com/areina/elfeed-cljsrn) - A mobile client for [Elfeed](https://github.com/skeeto/elfeed) rss reader, built with React Native.
|
||||
|
||||
* [Memory Hole](https://github.com/yogthos/memory-hole) - A small issue tracking app written with Luminus and re-frame.
|
||||
|
||||
* [Crossed](https://github.com/velveteer/crossed/) - A multiplayer crossword puzzle generator. Based on re-frame `0.7.0`
|
||||
|
||||
* [imperimetric](https://github.com/Dexterminator/imperimetric) - Webapp for converting texts with some system of measurement to another, such as imperial to metric.
|
||||
|
||||
* [mperimetric](https://github.com/Dexterminator/imperimetric) - Webapp for converting texts with some system of measurement to another, such as imperial to metric.
|
||||
* [Brave Clojure Open Source](https://github.com/braveclojure/open-source) A site using re-frame, liberator, boot and more to display active github projects that powers [http://open-source.braveclojure.com](http://open-source.braveclojure.com). Based on re-frame `0.6.0`
|
||||
|
||||
* [flux-challenge with re-frame](https://github.com/staltz/flux-challenge/tree/master/submissions/jelz) - flux-challenge is "a frontend challenge to test UI architectures and solutions". This is a ClojureScript + re-frame version. Based on re-frame `0.5.0`
|
||||
|
||||
* [flux-challenge with re-frame](https://github.com/staltz/flux-challenge/tree/master/submissions/jelz) - "a frontend challenge to test UI architectures and solutions". re-frame `0.5.0`
|
||||
* [fractalify](https://github.com/madvas/fractalify/) -
|
||||
An entertainment and educational webapp for creating & sharing fractal images that powers [fractalify.com](http://fractalify.com). Based on re-frame `0.4.1`
|
||||
|
||||
* [Angular Phonecat tutorial in re-frame](http://dhruvp.github.io/2015/03/07/re-frame/) - A detailed step-by-step tutorial that ports the Angular Phonecat tutorial to re-frame. Based on re-frame `0.2.0`
|
||||
|
||||
* [Braid](https://github.com/braidchat/braid) - A new approach to group chat, designed around conversations and tags instead of rooms.
|
||||
|
||||
### Effect and CoEffect Handlers
|
||||
@ -49,26 +36,27 @@ Please add to this list by submitting a pull request.
|
||||
* [re-frame-youtube-fx](https://github.com/micmarsh/re-frame-youtube-fx) - YouTube iframe API wrapper
|
||||
* [re-frame-web3-fx](https://github.com/madvas/re-frame-web3-fx) - Ethereum Web3 API
|
||||
* [re-frame-google-analytics-fx](https://github.com/madvas/re-frame-google-analytics-fx) - Google Analytics API
|
||||
* [re-frame-storage](https://github.com/akiroz/re-frame-storage) - Local Storage based persistence
|
||||
* [re-frame-storage-fx](https://github.com/deg/re-frame-storage-fx) - Another take on Local Storage persistence
|
||||
|
||||
### Routing
|
||||
|
||||
* (Bidirectional using Silk and Pushy)[https://pupeno.com/2015/08/18/no-hashes-bidirectional-routing-in-re-frame-with-silk-and-pushy/]
|
||||
* [Bidirectional using Silk and Pushy](https://pupeno.com/2015/08/18/no-hashes-bidirectional-routing-in-re-frame-with-silk-and-pushy/)
|
||||
|
||||
### Tools, Techniques & Libraries
|
||||
|
||||
* [re-frame-undo](https://github.com/Day8/re-frame-undo) - An undo library for re-frame
|
||||
* Animation using `react-flip-move` - http://www.upgradingdave.com/blog/posts/2016-12-17-permutation.html
|
||||
* [re-frame-test](https://github.com/Day8/re-frame-test) - Advanced testing utilities
|
||||
* [Animation](http://www.upgradingdave.com/blog/posts/2016-12-17-permutation.html) using `react-flip-move`
|
||||
* [re-frisk](https://github.com/flexsurfer/re-frisk) - A library for visualizing re-frame data and events.
|
||||
* [re-thread](https://github.com/yetanalytics/re-thread) - A library for running re-frame applications in Web Workers.
|
||||
* [re-frame-datatable](https://github.com/kishanov/re-frame-datatable) - DataTable UI component built for use with re-frame.
|
||||
* [Stately: State Machines](https://github.com/nodename/stately) also https://www.youtube.com/watch?v=klqorRUPluw
|
||||
* [re-frame-test](https://github.com/Day8/re-frame-test) - Integration Testing (not documented)
|
||||
* [re-learn](https://github.com/oliyh/re-learn) - Data driven tutorials for educating users of your reagent / re-frame app, built with re-frame
|
||||
* [re-learn](https://github.com/oliyh/re-learn) - Data driven tutorials for educating users of your reagent / re-frame app
|
||||
|
||||
### Videos
|
||||
|
||||
* [re-frame your ClojureScript applications](https://youtu.be/cDzjlx6otCU) - re-frame presentation given at Clojure/Conj 2016
|
||||
|
||||
* [A Video Tour of the Source Code of Ninja Tools](https://carouselapps.com/2015/12/02/tour-of-the-source-code-of-ninja-tools/)
|
||||
|
||||
### Server Side Rendering
|
||||
|
@ -89,8 +89,8 @@ transforms a `request` in one direction, and, then in the backwards
|
||||
sweep, it progressively produces a `response`.
|
||||
|
||||
In re-frame, the forwards sweep progressively creates the `coeffects`
|
||||
(inputs to the handler), while the backwards sweep processes the `effects`
|
||||
(outputs from the handler).
|
||||
(inputs to the event handler), while the backwards sweep processes the `effects`
|
||||
(outputs from the event handler).
|
||||
|
||||
I'll pause while you read that sentence again. That's the key
|
||||
concept, right there.
|
||||
|
@ -1,6 +1,8 @@
|
||||
|
||||
> In a rush? You can get away with skipping this page on the first pass. <br>
|
||||
> But remember to cycle back to it later. It contains useful insights.<br>
|
||||
> In a rush? You can skip this tutorial page on a first pass. <br>
|
||||
> It is quite abstract and it won't directly help you write re-frame code.
|
||||
> On the other hand, it will considerably deepen your understanding
|
||||
> of what re-frame is about, so remember to cycle back and read it later.<br>
|
||||
> Next page: [Effectful Handlers](EffectfulHandlers.md)
|
||||
|
||||
## Mental Model Omnibus
|
||||
@ -28,22 +30,6 @@ then those patterns will repeat themselves. <br>
|
||||
> -- Robert Pirsig, Zen and the Art of Motorcycle Maintenance
|
||||
|
||||
|
||||
<!-- 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)
|
||||
- [Interconnections](#interconnections)
|
||||
- [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
|
||||
@ -100,8 +86,66 @@ will encourage control logic into all the
|
||||
wrong places and you'll end up with a tire-fire of an Architecture. <br>
|
||||
Sincerely, The Self-appointed President of the Cursor Skeptic's Society.
|
||||
|
||||
## It does Event Sourcing
|
||||
## On DSLs and Machines
|
||||
|
||||
`Events` are cardinal to re-frame - they're a fundamental organising principle.
|
||||
|
||||
Each re-frame app will have a different set of `events` and your job is
|
||||
to design exactly the right ones for any given app you build. These `events`
|
||||
will model "intent" - generally the user's. They will be the
|
||||
"language of the system" and will provide the eloquence.
|
||||
|
||||
And they are data.
|
||||
|
||||
Imagine we created a drawing application. And then we allowed
|
||||
someone to use our application, and as they did we captured, into a collection,
|
||||
the events caused by that user's actions (button clicks, drags, key presses, etc).
|
||||
|
||||
The collection of events might look like this:
|
||||
```cljs
|
||||
(def collected-events
|
||||
[
|
||||
[:clear]
|
||||
[:new :triangle 1 2 3]
|
||||
[:select-object 23]
|
||||
[:rename "a better name"]
|
||||
[:delete-selection]
|
||||
....
|
||||
])
|
||||
```
|
||||
|
||||
Now, consider the following assembly instructions:
|
||||
```asm
|
||||
mov eax, ebx
|
||||
sub eax, 216
|
||||
mov BYTE PTR [ebx], 2
|
||||
```
|
||||
|
||||
Assembly instructions are represented as data, right? Data which happens to be "executable"
|
||||
by the right machine - an x86 machine in the case above.
|
||||
|
||||
I'd like you to now look back at that collection of events and view it in the
|
||||
same way - data instructions which can be executed - by the right machine.
|
||||
|
||||
Wait. What machine? Well, the Event Handlers you register collectively implement
|
||||
the "machine" on which these instructions execute. When you register a new event handler,
|
||||
it is like you are adding to the instruction set of the "machine".
|
||||
|
||||
In this repo's README, near the top, I explained that re-frame had a
|
||||
Data Oriented Design. Typically, that claim means there's a DSL (Domain specific language)
|
||||
involved and an interpreter for it. As you design your re-frame app,
|
||||
YOU design a DSL and then YOU provide the machine to execute it.
|
||||
|
||||
Summary:
|
||||
- Events are the assembly language of your app.
|
||||
- The instructions collectively form a Domain Specific Language (DSL). The language of your system.
|
||||
- These instructions are data.
|
||||
- One instruction after another gets executed by your functioning app.
|
||||
- The Event Handlers you register collectively implement the "machine" on which this DSL executes.
|
||||
|
||||
On the subject of DSLs, watch James Reeves' excellent talk (video): [Transparency through data](https://www.youtube.com/watch?v=zznwKCifC1A)
|
||||
|
||||
## It does Event Sourcing
|
||||
|
||||
How did that error happen, you puzzle, shaking your head ruefully?
|
||||
What did the user do immediately prior? What
|
||||
@ -120,7 +164,7 @@ 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`
|
||||
To find the bug, install the "checkpoint" state into `app-db`
|
||||
and then "play forward" through the collection of dispatched events.
|
||||
|
||||
The only way the app "moves forwards" is via events. "Replaying events" moves you
|
||||
@ -157,7 +201,7 @@ Then notice that `reg-event-db` event handlers take two arguments also:
|
||||
|
||||
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,
|
||||
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.
|
||||
@ -272,7 +316,7 @@ Sometimes, we'd rewrite this code as:
|
||||
str)
|
||||
```
|
||||
With this arrangement, we talk of "threading" data
|
||||
through functions. **It seems to help our comprehension to frame function
|
||||
through functions. **It seems to help our comprehension to conceive function
|
||||
composition in terms of data flow**.
|
||||
|
||||
re-frame delivers architecture
|
||||
@ -334,3 +378,8 @@ Next: [Infographic Overview](EventHandlingInfographic.md)
|
||||
[OM]:https://github.com/swannodette/om
|
||||
[Hoplon]:http://hoplon.io/
|
||||
[Pedestal App]:https://github.com/pedestal/pedestal-app
|
||||
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
@ -20,6 +20,19 @@
|
||||
- [Correcting a wrong](SubscriptionsCleanup.md)
|
||||
- [Flow Mechanics](SubscriptionFlow.md)
|
||||
|
||||
### Other Tutorials
|
||||
|
||||
- [purelyfunctional.tv](https://purelyfunctional.tv/guide/re-frame-building-blocks/) - a excellent, written overview.
|
||||
- [Lambda Island Videos](https://lambdaisland.com/episodes) - commercial videos on clojure, with some on re-frame
|
||||
|
||||
### Reagent Tutorials
|
||||
|
||||
- [The Basics](https://github.com/Day8/re-frame/wiki#reagent-tutorials) (look at the bottom of that page)
|
||||
- [Lambda Island Videos](https://lambdaisland.com/episodes). There's a 3 part series.
|
||||
- [purelyfunctional.tv ](https://purelyfunctional.tv/guide/reagent/) - a written overview
|
||||
- [Reagent Deep Dive Series from Timothy Pratley](http://timothypratley.blogspot.com.au/p/p.html) four part series
|
||||
- [Props, Children & Component Lifecycle](https://www.martinklepsch.org/posts/props-children-and-component-lifecycle-in-reagent.html) by Martin Klepsch
|
||||
|
||||
### App Structure
|
||||
|
||||
- [Basic App Structure](Basic-App-Structure.md)
|
||||
@ -51,6 +64,6 @@
|
||||
- [Code Of Conduct](Code-Of-Conduct.md)
|
||||
|
||||
<!-- We put these at the end so that there is nothing for doctoc to generate. -->
|
||||
<!-- START doctoc -->
|
||||
<!-- END doctoc -->
|
||||
<!-- START doctoc -->
|
||||
<!-- END doctoc -->
|
||||
|
||||
|
@ -71,7 +71,7 @@ Above, I suggested this:
|
||||
@(rf/subscribe [:time-str])])
|
||||
```
|
||||
|
||||
But that may offend your aesthetics. Too much noise with those `@`?
|
||||
But that may offend your aesthetics. Too much noise with those two `@`?
|
||||
|
||||
To clean this up, we can define a new `listen` function:
|
||||
```clj
|
||||
@ -91,6 +91,16 @@ And then rewrite:
|
||||
So, at the cost of writing your own function, `listen`, the code is now less noisy
|
||||
AND there's less chance of us forgetting an `@` (which can lead to odd problems).
|
||||
|
||||
### LambdaIsland Naming (LIN)
|
||||
|
||||
I've ended up quite liking [the alternative names](https://lambdaisland.com/blog/11-02-2017-re-frame-form-1-subscriptions)
|
||||
suggested by [Lambda Island Videos](https://lambdaisland.com/):
|
||||
|
||||
```cljs
|
||||
(def <sub (comp deref re-frame.core/subscribe)) ;; aka listen (above)
|
||||
(def >evt re-frame.core/dispatch)
|
||||
```
|
||||
|
||||
### Say It Again
|
||||
|
||||
So, if, in code review, you saw this view function:
|
||||
@ -102,8 +112,8 @@ So, if, in code review, you saw this view function:
|
||||
```
|
||||
What would you (supportively) object to?
|
||||
|
||||
That `sort`, right? Computation in the view. Instead, we want the right data
|
||||
delivered to the view - its job is to simply make `hiccup`.
|
||||
That `sort`, right? Computation in the view. Instead, we want exactly the right data
|
||||
delivered to the view - no further computation required - the view's job is to simply make `hiccup`.
|
||||
|
||||
The solution is to create a subscription that delivers items already sorted.
|
||||
```clj
|
||||
@ -134,15 +144,14 @@ Now it is easy to test `item-sorter` independently.
|
||||
|
||||
### And There's Another Benefit
|
||||
|
||||
re-frame de-duplicates signal graph nodes.
|
||||
re-frame de-duplicates signal graph nodes.
|
||||
|
||||
If, for example, two views wanted to `(subscribe [:sorted-items])` only the one node
|
||||
(in the signal graph) would be created. Only one node would be doing that
|
||||
potentially expensive sorting operation (when items changed) and values from
|
||||
it would be flowing through to both views.
|
||||
|
||||
That sort of efficiency can't happen if this views themselves are doing the `sort`.
|
||||
|
||||
That sort of efficiency can't happen if this views themselves are doing the `sort`.
|
||||
|
||||
### de-duplication
|
||||
|
||||
|
246
docs/Testing.md
246
docs/Testing.md
@ -1,39 +1,180 @@
|
||||
## Testing
|
||||
|
||||
This is an introductory, simple exploration of testing re-frame apps. If you want some more help see [re-frame-test](https://github.com/Day8/re-frame-test)
|
||||
This is an introduction to testing re-frame apps. It
|
||||
walks you through some choices.
|
||||
|
||||
## What To Test
|
||||
|
||||
With a re-frame app, there's principally three things to test:
|
||||
1. Event handlers
|
||||
2. Subscription handlers
|
||||
3. View functions
|
||||
For any re-frame app, there's three things to test:
|
||||
|
||||
## Event Handlers - Part 1
|
||||
- **Event Handlers** - most of your testing focus will
|
||||
be here because this is where most of the logic lives
|
||||
|
||||
- **Subscription Handlers** - often not a lot to test here. Only
|
||||
[Layer 2](SubscriptionInfographic.md) subscriptions need testing.
|
||||
|
||||
- **View functions** - I don't tend to write tests for views. There, I said it.
|
||||
Hey! It is mean to look at someone with that level of disapproval,
|
||||
while shaking your head. I have my reasons ...<br>
|
||||
In my experience with the re-frame architecture, View Functions
|
||||
tend to be an unlikely source of bugs. And every line of code you write is
|
||||
like a ball & chain you must forevermore drag about, so I dislike maintaining
|
||||
tests which don't deliver good bang for buck.
|
||||
|
||||
Event Handlers are pure functions and consequently easy to test.
|
||||
And, yes, in theory there's also `Effect Handlers` (Domino 3) to test,
|
||||
but you'll hardly ever write one, and, anyway, each one is different, so
|
||||
I've got no good general insight to offer you for them. They will be ignored
|
||||
in this tutorial.
|
||||
|
||||
## Test Terminology
|
||||
|
||||
First, create an event handler like this:
|
||||
Let's establish some terminology to aid the further explanations in this
|
||||
tutorial. Every unittest has 3 steps:
|
||||
1. **setup** initial conditions
|
||||
2. **execute** the thing-under-test
|
||||
3. **verify** that the thing-under-test did the right thing
|
||||
|
||||
## Exposing Event Handlers For Test
|
||||
|
||||
Event Handlers are pure functions which should make them easy to test, right?
|
||||
|
||||
First, create a named event handler using `defn` like this:
|
||||
```clj
|
||||
(defn my-db-handler
|
||||
[db v]
|
||||
(defn select-triangle
|
||||
[db [_ triangle-id]
|
||||
... return a modified version of db)
|
||||
```
|
||||
|
||||
Then, register it in a separate step:
|
||||
You'd register this handler in a separate step:
|
||||
```clj
|
||||
(re-frame.core/reg-event-db
|
||||
:some-id
|
||||
(re-frame.core/reg-event-db ;; this is a "-db" event handler, not "-fx"
|
||||
:select-triangle
|
||||
[some-interceptors]
|
||||
my-db-handler)
|
||||
select-triangle) ;; <--- defn above. don't use an annonomous fn
|
||||
```
|
||||
|
||||
With this setup, `my-db-handler` is available for direct testing.
|
||||
This arrangement means the event handler function
|
||||
`select-triangle` is readily available to be unittested.
|
||||
|
||||
Your unittests will pass in certain values for `db` and `v`,
|
||||
and then ensure it returns the right (modified version of) `db`.
|
||||
## Event Handlers - Setup - Part 1
|
||||
|
||||
To test `select-triangle`, a unittest must pass in values for the two arguments
|
||||
`db` and `v`. And, so, our **setup** would have to construct both values.
|
||||
|
||||
But how to create a useful `db` value?
|
||||
|
||||
`db` is a map of a certain structure, so one way would be to simply `assoc` values
|
||||
into a map at certain paths to simulate a real-world `db` value or, even easier, just use
|
||||
a map literal, like this:
|
||||
|
||||
```cljs
|
||||
;; a test
|
||||
(let [
|
||||
;; setup - create db and event
|
||||
db {:some 42 :thing "hello"} ; a literal
|
||||
event [:select-triange :other :event :args]
|
||||
|
||||
;; execute
|
||||
result-db (select-triange db event)]
|
||||
|
||||
;; validate that result-db is correct)
|
||||
(is ...)
|
||||
```
|
||||
|
||||
This certainly works in theory, but in practice,
|
||||
unless we are careful, constructing the `db`
|
||||
value in **setup** could:
|
||||
* be manual and time consuming
|
||||
* tie tests to the internal structure of `app-db`
|
||||
|
||||
The **setup** of every test could end up relying on the internal structure
|
||||
of `app-db` and any change in that structure (which is inevitable over time)
|
||||
would result in a lot re-work in the tests. That's too fragile.
|
||||
|
||||
So, this approach doesn't quite work.
|
||||
|
||||
## Event Handlers - Setup - Part 2
|
||||
|
||||
> In re-frame, `Events` are central. They are the "language of the system". They
|
||||
provide the eloquence.
|
||||
|
||||
|
||||
The `db` value (stored in `app-db`) is the cumulative result
|
||||
of many event handlers running.
|
||||
|
||||
We can use this idea. In **setup**, instead of manually trying to create that `db` value, we could
|
||||
"build up" a `db` value by threading `db` through many event handlers
|
||||
which cumulatively create the required initial state. Tests then need
|
||||
know nothing about the internal structure of that `db`.
|
||||
|
||||
Like this:
|
||||
```clj
|
||||
(let [
|
||||
;; setup - cummulatively build up db
|
||||
db (-> {} ;; empty db
|
||||
(initialise-db [:initialise-db]) ;; each event handler expects db and event
|
||||
(clear-panel [:clear-panel])
|
||||
(draw-triangle [:draw-triangle 1 2 3]))
|
||||
|
||||
event [:select-triange :other :stuff]
|
||||
|
||||
;; now execute the event handler under test
|
||||
db' (select-triange db event)]
|
||||
|
||||
;; validate that db' is correct
|
||||
(is ...)
|
||||
```
|
||||
|
||||
This approach works so long as all the event handlers are
|
||||
of the `-db` kind, but the threading gets a little messy when some event
|
||||
handlers are of the `-fx` kind which take a `coeffect` argument and
|
||||
return `effects`, instead of a `db` value.
|
||||
|
||||
So, this approach is quite workable in some cases, but can get messy
|
||||
in the general case.
|
||||
|
||||
## Event Handlers - Setup - Part 3
|
||||
|
||||
There is further variation which is quite general but not as pure.
|
||||
|
||||
During test **setup** we could literally just `dispatch` the events
|
||||
which would put `app-db` into the right state.
|
||||
|
||||
Except, we'd have to use `dispatch-sysnc` rather `dispatch` to
|
||||
force immediate handling of events, rather than queuing.
|
||||
|
||||
```clj
|
||||
;; setup - cummulatively build up db
|
||||
(dispatch-sync [:initialise-db])
|
||||
(dispatch-sync [:clear-panel])
|
||||
(dispatch-sync [:draw-triangle 1 2 3]))
|
||||
|
||||
;; execute
|
||||
(dispatch-sync [:select-triange :other :stuff])
|
||||
|
||||
;; validate that the valuein `app-db` is correct
|
||||
;; perhaps with subscriptions
|
||||
```
|
||||
|
||||
Notes:
|
||||
1. we use `dispatch-sync` because `dispatch` is async (event is handled not now, but soon)
|
||||
2. Not pure. We are choosing to mutate the global `app-db`. But
|
||||
having said that, there's something about this approach with is remarkably
|
||||
pragmatic.
|
||||
2. the **setup** is now very natural. The associated handlers can be either `-db` or `-fx`
|
||||
3. if the handlers have effects other than just updating app-db, we might need to stub out XXX
|
||||
4. How do we look at the results ????
|
||||
|
||||
If this method appeals to you, you should ABSOLUTELY review the utilities in this helper library:
|
||||
[re-frame-test](https://github.com/Day8/re-frame-test).
|
||||
|
||||
In summary, event handlers should be easy to test because they are pure functions. The interesting
|
||||
part is the unittest "setup" where we need to establishing an initial value for `db`.
|
||||
|
||||
## 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):
|
||||
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
|
||||
@ -55,7 +196,7 @@ Here's a subscription handler from [the todomvc example](https://github.com/Day8
|
||||
|
||||
How do we test this?
|
||||
|
||||
We could split the computation function from its registration, like this:
|
||||
First, we could split the computation function from its registration, like this:
|
||||
```clj
|
||||
(defn visible-todos
|
||||
[[todos showing] _]
|
||||
@ -74,25 +215,17 @@ We could split the computation function from its registration, like this:
|
||||
visible-todos) ;; <--- computation function used here
|
||||
```
|
||||
|
||||
That makes `visible-todos` available for direct unit testing.
|
||||
That makes `visible-todos` available for direct unit testing.
|
||||
|
||||
## View Functions - Part 1
|
||||
|
||||
Components/views are slightly more tricky. There's a few options.
|
||||
Components/views are more tricky and there are 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.
|
||||
But remember my ugly secret - I don't tend to write tests for my views.
|
||||
|
||||
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.
|
||||
But here's how, theoretically, I'd write tests if I wasn't me ...
|
||||
|
||||
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)
|
||||
If a View Function 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:
|
||||
@ -105,17 +238,19 @@ A trivial example:
|
||||
;;=> [:div "Hello " "Wiki"]
|
||||
```
|
||||
|
||||
So, here, testing involves passing values into the function and checking the data structure returned for correctness.
|
||||
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.
|
||||
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.
|
||||
|
||||
|
||||
## View Functions - Part 2A
|
||||
|
||||
But what if the View Function 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)?
|
||||
But what if the View Function has a subscription?
|
||||
|
||||
```clj
|
||||
(defn my-view
|
||||
@ -124,10 +259,10 @@ But what if the View Function has a subscription (via a [Form-2](https://github.
|
||||
[:div .... using @val in here])))
|
||||
```
|
||||
|
||||
There's no immediately obvious way to test this as a lovely pure function. Because it is not pure.
|
||||
The use of `subscribe` makes the function impure (it obtains data from places other than its args).
|
||||
|
||||
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)
|
||||
A testing plan might be:
|
||||
1. setup `app-db` with some values in the right places (via dispatch of events?)
|
||||
2. call `my-view` (with a parameter) which will return hiccup
|
||||
3. check the hiccup structure for correctness.
|
||||
|
||||
@ -136,16 +271,12 @@ Continuing on, in a second phase you could then:
|
||||
6. call view functions 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.
|
||||
Which is all possible, if a little messy.
|
||||
|
||||
## View Functions - Part 2B
|
||||
|
||||
Or ... instead of the not-very-pure method above, you could use `with-redefs` on `subscribe` to stub out re-frame altogether:
|
||||
|
||||
There is a very pragmatic method available to handle the impurity: use `with-redefs`
|
||||
to stub out `subscribe`. Like this:
|
||||
```clj
|
||||
(defn subscription-stub [x]
|
||||
(atom
|
||||
@ -154,27 +285,29 @@ Or ... instead of the not-very-pure method above, you could use `with-redefs` o
|
||||
|
||||
(deftest some-test
|
||||
(with-redefs [re-frame/subscribe (subscription-stub)]
|
||||
(testing "some rendering"
|
||||
..... somehow call or render the component and check the output)))
|
||||
(testing "some some view which does a subscribe"
|
||||
..... call the view function and the hiccup 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.
|
||||
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.
|
||||
|
||||
## View Functions - Part 2C
|
||||
|
||||
Or ... you can structure in the first place for easier testing and pure functions.
|
||||
Or ... there is another option: you can structure in the first place for pure view 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).
|
||||
(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.
|
||||
easily tested. The outer is impure but fairly trivial.
|
||||
|
||||
To get a more concrete idea, I'll direct you to another page on this Wiki
|
||||
To get a more concrete idea, I'll direct you to another page in the docs
|
||||
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]]
|
||||
|
||||
pattern for a different purpose:
|
||||
[Using Stateful JS Components](Using-Stateful-JS-Components.md)
|
||||
|
||||
Note this technique could be made simple and almost invisible via the
|
||||
use of macros. (Contribute one if you have it).
|
||||
|
||||
@ -184,9 +317,8 @@ it is called the [Container/Component pattern](https://medium.com/@learnreact/co
|
||||
|
||||
## 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!
|
||||
Event handlers will be your primary focus when testing. Remember to review the utilities in
|
||||
[re-frame-test](https://github.com/Day8/re-frame-test).
|
||||
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
@ -5,7 +5,7 @@ This tiny application is meant to provide a quick start of the basics of re-fram
|
||||
A detailed source code walk-through is provided in the docs:
|
||||
https://github.com/Day8/re-frame/blob/master/docs/CodeWalkthrough.md
|
||||
|
||||
All the code is in one namespace: `/src/simpleexample/core.cljs`
|
||||
All the code is in one namespace: `/src/simple/core.cljs`.
|
||||
|
||||
### Run It And Change It
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
:dependencies [[org.clojure/clojure "1.8.0"]
|
||||
[org.clojure/clojurescript "1.9.227"]
|
||||
[reagent "0.6.0-rc"]
|
||||
[re-frame "0.9.0"]]
|
||||
[re-frame "0.9.4"]]
|
||||
|
||||
:plugins [[lein-cljsbuild "1.1.3"]
|
||||
[lein-figwheel "0.5.4-7"]]
|
||||
|
@ -14,7 +14,7 @@
|
||||
(rf/dispatch [:timer now]))) ;; <-- dispatch used
|
||||
|
||||
;; Call the dispatching function every second.
|
||||
;; `defonce` is like `def` but it ensures only instance is ever
|
||||
;; `defonce` is like `def` but it ensures only one instance is ever
|
||||
;; created in the face of figwheel hot-reloading of this file.
|
||||
(defonce do-timer (js/setInterval dispatch-timer-event 1000))
|
||||
|
||||
@ -45,8 +45,7 @@
|
||||
(rf/reg-sub
|
||||
:time
|
||||
(fn [db _] ;; db is current app state. 2nd unused param is query vector
|
||||
(-> db
|
||||
:time)))
|
||||
(:time db))) ;; return a query computation over the application state
|
||||
|
||||
(rf/reg-sub
|
||||
:time-color
|
||||
|
@ -2,7 +2,7 @@
|
||||
:dependencies [[org.clojure/clojure "1.8.0"]
|
||||
[org.clojure/clojurescript "1.9.89"]
|
||||
[reagent "0.6.0-rc"]
|
||||
[re-frame "0.9.0"]
|
||||
[re-frame "0.9.4"]
|
||||
[binaryage/devtools "0.8.1"]
|
||||
[secretary "1.2.3"]]
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
||||
(def ->local-store (after todos->local-store))
|
||||
|
||||
;; Each event handler can have its own set of interceptors (middleware)
|
||||
;; But we use the same set of interceptors for all event habdlers related
|
||||
;; But we use the same set of interceptors for all event handlers related
|
||||
;; to manipulating todos.
|
||||
;; A chain of interceptors is a vector.
|
||||
(def todo-interceptors [check-spec-interceptor ;; ensure the spec is still valid
|
||||
|
@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2016 Michael Thompson
|
||||
Copyright (c) 2015-2017 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
|
||||
|
@ -1,4 +1,4 @@
|
||||
(defproject re-frame "0.9.4-SNAPSHOT"
|
||||
(defproject re-frame "0.9.5-SNAPSHOT"
|
||||
:description "A Clojurescript MVC-like Framework For Writing SPAs Using Reagent."
|
||||
:url "https://github.com/Day8/re-frame.git"
|
||||
:license {:name "MIT"}
|
||||
|
@ -85,8 +85,8 @@
|
||||
:dispatch-n
|
||||
(fn [value]
|
||||
(if-not (sequential? value)
|
||||
(console :error "re-frame: ignoring bad :dispatch-n value. Expected a collection, got got:" value))
|
||||
(doseq [event value] (router/dispatch event))))
|
||||
(console :error "re-frame: ignoring bad :dispatch-n value. Expected a collection, got got:" value)
|
||||
(doseq [event value] (router/dispatch event)))))
|
||||
|
||||
|
||||
;; :deregister-event-handler
|
||||
@ -104,8 +104,8 @@
|
||||
(fn [value]
|
||||
(let [clear-event (partial clear-handlers events/kind)]
|
||||
(if (sequential? value)
|
||||
(doseq [event (if (sequential? value) value [value])]
|
||||
(clear-event event))))))
|
||||
(doseq [event value] (clear-event event))
|
||||
(clear-event value)))))
|
||||
|
||||
|
||||
;; :db
|
||||
@ -118,5 +118,6 @@
|
||||
(register
|
||||
:db
|
||||
(fn [value]
|
||||
(reset! app-db value)))
|
||||
(if-not (identical? @app-db value)
|
||||
(reset! app-db value))))
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
(ns re-frame.interceptor
|
||||
(:require
|
||||
[re-frame.loggers :refer [console]]
|
||||
[re-frame.interop :refer [ratom? empty-queue debug-enabled?]]
|
||||
[clojure.set :as set]))
|
||||
[re-frame.loggers :refer [console]]
|
||||
[re-frame.interop :refer [empty-queue debug-enabled?]]))
|
||||
|
||||
|
||||
(def mandatory-interceptor-keys #{:id :after :before})
|
||||
|
@ -211,7 +211,7 @@
|
||||
eddting operation. Nice and efficient, but fiddly. A bug generator
|
||||
approach.
|
||||
|
||||
So, instead, we create an `f` which recalcualtes warnings from scratch
|
||||
So, instead, we create an `f` which recalculates warnings from scratch
|
||||
every time there is ANY change. It will inspect all the todos, and
|
||||
reset ALL FLAGS every time (overwriting what was there previously)
|
||||
and fully recalculate the list of duplicates (displayed at the bottom?).
|
||||
|
Loading…
x
Reference in New Issue
Block a user