Merge branch 'master' into master

This commit is contained in:
Mike Thompson 2017-07-16 19:45:50 +10:00 committed by GitHub
commit bcd852d089
21 changed files with 469 additions and 222 deletions

View File

@ -1,9 +1,19 @@
## Unreleased ## 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 #### 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) - 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) ## 0.9.3 (2017.05.15)

15
CITATION.md Normal file
View File

@ -0,0 +1,15 @@
To cite re-frame in publications, please use:
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.801613.svg)](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
View File

@ -18,6 +18,16 @@ y'know. Pretty good.
[![Circle CI](https://circleci.com/gh/Day8/re-frame/tree/develop.svg?style=shield&circle-token=:circle-ci-badge-token)](https://circleci.com/gh/Day8/re-frame/tree/develop) [![Circle CI](https://circleci.com/gh/Day8/re-frame/tree/develop.svg?style=shield&circle-token=:circle-ci-badge-token)](https://circleci.com/gh/Day8/re-frame/tree/develop)
[![Circle CI](https://circleci.com/gh/Day8/re-frame/tree/master.svg?style=shield&circle-token=:circle-ci-badge-token)](https://circleci.com/gh/Day8/re-frame/tree/master) [![Circle CI](https://circleci.com/gh/Day8/re-frame/tree/master.svg?style=shield&circle-token=:circle-ci-badge-token)](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? ## Why Should You Care?
@ -36,10 +46,10 @@ Perhaps:
In this space, re-frame is very old, hopefully in a Gandalf kind of way. 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, 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 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, re-frame has pioneered ideas like event handler middleware,
coeffect accretion, and de-duplicated signal graphs. 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, unidirectional data flow, pristinely pure functions,
interceptors, coeffects, conveyor belts, statechart-friendliness (FSM) interceptors, coeffects, conveyor belts, statechart-friendliness (FSM)
and claims an immaculate hammock conception. It also has a charming 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.** **Data - that's the way we roll.**
## re-frame ## It is a loop
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
Architecturally, re-frame implements "a perpetual 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 Computationally, each iteration of the loop involves a
six domino cascade. six domino cascade.
One domino triggers the next, which triggers the next, et cetera, until we are One domino triggers the next, which triggers the next, et cetera, boom, boom, boom, until we are
back at the beginning of the loop, whereupon the dominoes spring to attention back at the beginning of the loop, and the dominoes spring to attention
again, ready for the next iteration of the same cascade. 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 ### 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. "descriptions of required DOM", returned by the view functions of Domino 5, are made real.
The browser DOM nodes are mutated. The browser DOM nodes are mutated.
## Managing mutation
### 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
The two sub-cascades 1-2-3 and 4-5-6 have a similar structure. 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 In both cases, you don't need to worry yourself about this dirty work. re-frame looks
after those dominoes. 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"> <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 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. 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 **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. 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 ### Code For Domino 1
The delete button for that 3rd item will have an `on-click` handler (function) which looks The delete button for that 3rd item will be rendered by a ViewFunction which looks like this:
like this:
```clj ```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, A re-frame `event` is a vector and, in this case,
it has 2 elements: `[:delete-item 2486]`. The first element, it has 2 elements: `[:delete-item 2486]` (where `2486` in the made-up id for that 3rd item).
`: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. 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 ### 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]`. compute the `effect` of the event `[:delete-item 2486]`.
Earlier, on program startup, `h` would have been On app startup, `re-frame.core/reg-event-fx` would have been used to
registered for handling `:delete-item` `events` like this: register this `h` as the handler for `:delete-item` events, like this:
```clj ```clj
(re-frame.core/reg-event-fx ;; a part of the re-frame API (re-frame.core/reg-event-fx ;; a part of the re-frame API
:delete-item ;; the kind of event :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: `h` is written to take two arguments:
1. a `coeffects` map which contains the current state of the world (including app state) 1. a `coeffects` map which contains the current state of the world (including app state)
2. the `event` 2. the `event` to handle
`h` returns a map of `effects` - a description It is the job of `h` to compute how the world should be changed by the event, and
of how the world should be changed by the event. it returns a map of `effects` - a description of the those changes.
Here's a sketch (we are at 30,000 feet): Here's a sketch (we are at 30,000 feet):
```clj ```clj
(defn h (defn h ;; choose a better name like delete-item
[{:keys [db]} event] ;; args: db from coeffect, event [coeffects event] ;; args: db from coeffect, event
(let [item-id (second event)] ;; extract id from event vector (let [item-id (second event) ;; extract id from event vector
{:db (dissoc-in db [:items item-id])})) ;; effect is change db 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 of the world into that first `coeffects` argument (map). Different
event handlers need to know different things about the world event handlers need different "things" to get their job done. But
in order to get their job done. But current "application state" current "application state" is one aspect of the world which is
is one aspect of the world which is invariably needed, and it is made invariably needed, and it is available by default in the `:db` key.
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 ### Code For Domino 3
@ -339,7 +358,7 @@ An `effect handler` (function) actions the `effects` returned by `h`.
Here's what `h` returned: Here's what `h` returned:
```clj ```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 Each key of the map identifies one kind
of `effect`, and the value for that key supplies further details. 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. 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 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: Just to be clear, if `h` had returned:
```clj ```clj
{:wear {:pants "velour flares" :belt false} {:wear {:pants "velour flares" :belt false}
:tweet "Okay, yes, I am Satoshi. #coverblown"} :tweet "Okay, yes, I am Satoshi. #coverblown"}
``` ```
Then the two effects handlers registered for `:wear` and `:tweet` would 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 be called to action those two effects. And, no, re-frame
does not supply standard effect handlers for either, so you would have had does not supply standard effect handlers for either, so you would have had
to have written them yourself (see how in a later tutorial). to have written them yourself (see how in a later tutorial).
### Code For Domino 4 ### Code For Domino 4
Because an effect handler just updated "app state", Because an effect handler just mutated "application state",
a query (function) over this app state is called automatically (reactively), a query (function) over this app state is automatically called (reactively).
itself computing the list of items.
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 Because the items are stored in app state, there's not a lot
to compute in this case. This to compute and, instead, it acts more like an extractor or accessor,
query function acts more like an extractor or accessor: just plucking the list of items out of application state:
```clj ```clj
(defn query-fn (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 (:items db)) ;; not much of a materialised view
``` ```
On program startup, such a `query-fn` must be associated with a `query-id`, 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 ```clj
(re-frame.core/reg-sub ;; part of the re-frame API (re-frame.core/reg-sub ;; part of the re-frame API
:query-items ;; query id :query-items ;; query id
query-fn) ;; query fn 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". use `query-fn` to compute it".
### Code For Domino 5 ### Code For Domino 5
Because the query function for `:query-items` just re-computed a new value, Because the query function for `:query-items` just re-computed a new value,
any view (function) which subscribes to `:query-items` any view (function) which uses a `(subscribe [:query-items])`
is called automatically (reactively) to re-compute DOM. is called automatically (reactively) to re-compute new DOM.
View functions compute a data structure, in hiccup format, describing View functions compute a data structure, in hiccup format, describing
the DOM nodes required. In this case, there will be no DOM nodes the DOM nodes required. In this case, the view functions will *not* be generating
for the now-deleted item, obviously, but otherwise the same DOM as last time. hiccup for the now-deleted item obviously but, other than this,
the hiccup computed will be the same as last time.
```clj ```clj
(defn items-view (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 [:div (map item-render @items)])) ;; assume item-render already written
``` ```
Notice how `items` is "sourced" from "app state" via `subscribe`. Notice how `items` is "sourced" from "app state" via `re-frame.core/subscribe`.
It is called with a query id to identify what data it needs. 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 ### Code For Domino 6
@ -444,7 +486,7 @@ When building a re-frame app, you:
- write Reagent view functions (view layer) (domino 5) - 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 re-frame was released in early 2015, and has since
[been](https://www.fullcontact.com) successfully [been](https://www.fullcontact.com) successfully
@ -475,28 +517,16 @@ and useful 3rd party libraries.
## Where Do I Go Next? ## Where Do I Go Next?
**At this point you At this point, you know 50% of re-frame. The full [docs are here](/docs/README.md).
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.
Next you need to read the other three articles in the [Introduction section](/docs#introduction): There are two example apps to play with: <br>
* [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>
https://github.com/Day8/re-frame/tree/master/examples https://github.com/Day8/re-frame/tree/master/examples
Use a template to create your own project: <br> Use a template to create your own project: <br>
Client only: https://github.com/Day8/re-frame-template <br> Client only: https://github.com/Day8/re-frame-template <br>
Full Stack: http://www.luminusweb.net/ 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 https://github.com/Day8/re-frame/blob/develop/docs/External-Resources.md
### T-Shirt Unlocked ### T-Shirt Unlocked

View File

@ -7,3 +7,5 @@ test:
override: override:
- lein karma-once - lein karma-once
- karma start --single-run --reporters junit,dots - karma start --single-run --reporters junit,dots
- lein cljsbuild once:
pwd: examples/todomvc

View File

@ -30,7 +30,7 @@ Then:
1. `git clone https://github.com/Day8/re-frame.git` 1. `git clone https://github.com/Day8/re-frame.git`
2. `cd re-frame/examples/simple` 2. `cd re-frame/examples/simple`
3. `lein do clean, figwheel` 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 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) put into `/resources/public/js/client.js` which is loaded into `/resources/public/example.html` (the HTML you just opened)

View File

@ -6,37 +6,24 @@ Please add to this list by submitting a pull request.
### Templates ### Templates
* [re-frame-template](https://github.com/Day8/re-frame-template) - Generates the client side SPA * [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. * [Luminus](http://www.luminusweb.net) - Generates SPA plus server side.
* [re-natal](https://github.com/drapanjanas/re-natal) - React Native apps. * [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. Based on re-frame `0.7.0`
* [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.
* [Celibidache](https://github.com/velveteer/celibidache/) - An opinionated starter for re-frame applications using Boot. 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 ### 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. * [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. * [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. * [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` * [Crossed](https://github.com/velveteer/crossed/) - A multiplayer crossword puzzle generator. Based on re-frame `0.7.0`
* [mperimetric](https://github.com/Dexterminator/imperimetric) - Webapp for converting texts with some system of measurement to another, such as imperial to metric.
* [imperimetric](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` * [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) - "a frontend challenge to test UI architectures and solutions". re-frame `0.5.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`
* [fractalify](https://github.com/madvas/fractalify/) - * [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` 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` * [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. * [Braid](https://github.com/braidchat/braid) - A new approach to group chat, designed around conversations and tags instead of rooms.
### Effect and CoEffect Handlers ### 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-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-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-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 ### 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 ### Tools, Techniques & Libraries
* [re-frame-undo](https://github.com/Day8/re-frame-undo) - An undo library for re-frame * [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-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-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. * [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 * [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
* [re-learn](https://github.com/oliyh/re-learn) - Data driven tutorials for educating users of your reagent / re-frame app, built with re-frame
### Videos ### Videos
* [re-frame your ClojureScript applications](https://youtu.be/cDzjlx6otCU) - re-frame presentation given at Clojure/Conj 2016 * [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/) * [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 ### Server Side Rendering

View File

@ -89,8 +89,8 @@ transforms a `request` in one direction, and, then in the backwards
sweep, it progressively produces a `response`. sweep, it progressively produces a `response`.
In re-frame, the forwards sweep progressively creates the `coeffects` In re-frame, the forwards sweep progressively creates the `coeffects`
(inputs to the handler), while the backwards sweep processes the `effects` (inputs to the event handler), while the backwards sweep processes the `effects`
(outputs from the handler). (outputs from the event handler).
I'll pause while you read that sentence again. That's the key I'll pause while you read that sentence again. That's the key
concept, right there. concept, right there.

View File

@ -1,6 +1,8 @@
> In a rush? You can get away with skipping this page on the first pass. <br> > In a rush? You can skip this tutorial page on a first pass. <br>
> But remember to cycle back to it later. It contains useful insights.<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) > Next page: [Effectful Handlers](EffectfulHandlers.md)
## Mental Model Omnibus ## Mental Model Omnibus
@ -28,22 +30,6 @@ then those patterns will repeat themselves. <br>
> -- Robert Pirsig, Zen and the Art of Motorcycle Maintenance > -- 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? ## What is the problem?
First, we decided to build our SPA apps with ClojureScript, then we 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> 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. 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? How did that error happen, you puzzle, shaking your head ruefully?
What did the user do immediately prior? What 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. If you have that data, then you can reproduce the error.
re-frame allows you to time travel, even in a production setting. 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. and then "play forward" through the collection of dispatched events.
The only way the app "moves forwards" is via events. "Replaying events" moves you 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`!! 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 value in `app-db` is the result of performing a `reduce` over
the entire `collection` of events dispatched in the app up until 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. 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) str)
``` ```
With this arrangement, we talk of "threading" data 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**. composition in terms of data flow**.
re-frame delivers architecture re-frame delivers architecture
@ -334,3 +378,8 @@ Next: [Infographic Overview](EventHandlingInfographic.md)
[OM]:https://github.com/swannodette/om [OM]:https://github.com/swannodette/om
[Hoplon]:http://hoplon.io/ [Hoplon]:http://hoplon.io/
[Pedestal App]:https://github.com/pedestal/pedestal-app [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 -->

View File

@ -20,6 +20,19 @@
- [Correcting a wrong](SubscriptionsCleanup.md) - [Correcting a wrong](SubscriptionsCleanup.md)
- [Flow Mechanics](SubscriptionFlow.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 ### App Structure
- [Basic App Structure](Basic-App-Structure.md) - [Basic App Structure](Basic-App-Structure.md)
@ -51,6 +64,6 @@
- [Code Of Conduct](Code-Of-Conduct.md) - [Code Of Conduct](Code-Of-Conduct.md)
<!-- We put these at the end so that there is nothing for doctoc to generate. --> <!-- We put these at the end so that there is nothing for doctoc to generate. -->
<!-- START doctoc --> <!-- START doctoc -->
<!-- END doctoc --> <!-- END doctoc -->

View File

@ -71,7 +71,7 @@ Above, I suggested this:
@(rf/subscribe [:time-str])]) @(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: To clean this up, we can define a new `listen` function:
```clj ```clj
@ -91,6 +91,16 @@ And then rewrite:
So, at the cost of writing your own function, `listen`, the code is now less noisy 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). 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 ### Say It Again
So, if, in code review, you saw this view function: 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? What would you (supportively) object to?
That `sort`, right? Computation in the view. Instead, we want the right data That `sort`, right? Computation in the view. Instead, we want exactly the right data
delivered to the view - its job is to simply make `hiccup`. 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. The solution is to create a subscription that delivers items already sorted.
```clj ```clj
@ -134,15 +144,14 @@ Now it is easy to test `item-sorter` independently.
### And There's Another Benefit ### 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 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 (in the signal graph) would be created. Only one node would be doing that
potentially expensive sorting operation (when items changed) and values from potentially expensive sorting operation (when items changed) and values from
it would be flowing through to both views. 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 ### de-duplication

View File

@ -1,39 +1,180 @@
## Testing ## 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: For any re-frame app, there's three things to test:
1. Event handlers
2. Subscription handlers
3. View functions
## 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 ```clj
(defn my-db-handler (defn select-triangle
[db v] [db [_ triangle-id]
... return a modified version of db) ... return a modified version of db)
``` ```
Then, register it in a separate step: You'd register this handler in a separate step:
```clj ```clj
(re-frame.core/reg-event-db (re-frame.core/reg-event-db ;; this is a "-db" event handler, not "-fx"
:some-id :select-triangle
[some-interceptors] [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`, ## Event Handlers - Setup - Part 1
and then ensure it returns the right (modified version of) `db`.
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 ## 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 ```clj
(reg-sub (reg-sub
@ -55,7 +196,7 @@ Here's a subscription handler from [the todomvc example](https://github.com/Day8
How do we test this? 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 ```clj
(defn visible-todos (defn visible-todos
[[todos showing] _] [[todos showing] _]
@ -74,25 +215,17 @@ We could split the computation function from its registration, like this:
visible-todos) ;; <--- computation function used here 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 ## 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. But remember my 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 But here's how, theoretically, I'd write tests if I wasn't me ...
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!! If a View Function is a [Form-1](https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components#form-1-a-simple-function)
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. structure, then it is fairly easy to test.
A trivial example: A trivial example:
@ -105,17 +238,19 @@ A trivial example:
;;=> [:div "Hello " "Wiki"] ;;=> [: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? 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. 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 ## 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 ```clj
(defn my-view (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]))) [: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: A testing plan might be:
1. setup `app-db` with some values in the right places (for the subscription) 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 2. call `my-view` (with a parameter) which will return hiccup
3. check the hiccup structure for correctness. 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). 6. call view functions again (hiccup returned).
7. check that the hiccup 7. check that the hiccup
Which is all possible, if a little messy, and with one gotcha. After you change the Which is all possible, if a little messy.
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.
## View Functions - Part 2B ## 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 ```clj
(defn subscription-stub [x] (defn subscription-stub [x]
(atom (atom
@ -154,27 +285,29 @@ Or ... instead of the not-very-pure method above, you could use `with-redefs` o
(deftest some-test (deftest some-test
(with-redefs [re-frame/subscribe (subscription-stub)] (with-redefs [re-frame/subscribe (subscription-stub)]
(testing "some rendering" (testing "some some view which does a subscribe"
..... somehow call or render the component and check the output))) ..... call the view function and the hiccup output)))
``` ```
For more integration level testing, you can use `with-mounted-component` 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 ## 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 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 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` 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 Note this technique could be made simple and almost invisible via the
use of macros. (Contribute one if you have it). 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 ## Summary
So, we stumbled slightly at the final hurdle with Form-2 Components. But prior Event handlers will be your primary focus when testing. Remember to review the utilities in
to this, the testing story for re-frame was as good as it gets: you are testing [re-frame-test](https://github.com/Day8/re-frame-test).
a bunch of simple, pure functions. No dependency injection in sight!
<!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- START doctoc generated TOC please keep comment here to allow auto update -->

View File

@ -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: A detailed source code walk-through is provided in the docs:
https://github.com/Day8/re-frame/blob/master/docs/CodeWalkthrough.md 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 ### Run It And Change It

View File

@ -2,7 +2,7 @@
:dependencies [[org.clojure/clojure "1.8.0"] :dependencies [[org.clojure/clojure "1.8.0"]
[org.clojure/clojurescript "1.9.227"] [org.clojure/clojurescript "1.9.227"]
[reagent "0.6.0-rc"] [reagent "0.6.0-rc"]
[re-frame "0.9.0"]] [re-frame "0.9.4"]]
:plugins [[lein-cljsbuild "1.1.3"] :plugins [[lein-cljsbuild "1.1.3"]
[lein-figwheel "0.5.4-7"]] [lein-figwheel "0.5.4-7"]]

View File

@ -14,7 +14,7 @@
(rf/dispatch [:timer now]))) ;; <-- dispatch used (rf/dispatch [:timer now]))) ;; <-- dispatch used
;; Call the dispatching function every second. ;; 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. ;; created in the face of figwheel hot-reloading of this file.
(defonce do-timer (js/setInterval dispatch-timer-event 1000)) (defonce do-timer (js/setInterval dispatch-timer-event 1000))
@ -45,8 +45,7 @@
(rf/reg-sub (rf/reg-sub
:time :time
(fn [db _] ;; db is current app state. 2nd unused param is query vector (fn [db _] ;; db is current app state. 2nd unused param is query vector
(-> db (:time db))) ;; return a query computation over the application state
:time)))
(rf/reg-sub (rf/reg-sub
:time-color :time-color

View File

@ -2,7 +2,7 @@
:dependencies [[org.clojure/clojure "1.8.0"] :dependencies [[org.clojure/clojure "1.8.0"]
[org.clojure/clojurescript "1.9.89"] [org.clojure/clojurescript "1.9.89"]
[reagent "0.6.0-rc"] [reagent "0.6.0-rc"]
[re-frame "0.9.0"] [re-frame "0.9.4"]
[binaryage/devtools "0.8.1"] [binaryage/devtools "0.8.1"]
[secretary "1.2.3"]] [secretary "1.2.3"]]

View File

@ -26,7 +26,7 @@
(def ->local-store (after todos->local-store)) (def ->local-store (after todos->local-store))
;; Each event handler can have its own set of interceptors (middleware) ;; 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. ;; to manipulating todos.
;; A chain of interceptors is a vector. ;; A chain of interceptors is a vector.
(def todo-interceptors [check-spec-interceptor ;; ensure the spec is still valid (def todo-interceptors [check-spec-interceptor ;; ensure the spec is still valid

View File

@ -1,6 +1,6 @@
The MIT License (MIT) 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -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." :description "A Clojurescript MVC-like Framework For Writing SPAs Using Reagent."
:url "https://github.com/Day8/re-frame.git" :url "https://github.com/Day8/re-frame.git"
:license {:name "MIT"} :license {:name "MIT"}

View File

@ -85,8 +85,8 @@
:dispatch-n :dispatch-n
(fn [value] (fn [value]
(if-not (sequential? value) (if-not (sequential? value)
(console :error "re-frame: ignoring bad :dispatch-n value. Expected a collection, got got:" value)) (console :error "re-frame: ignoring bad :dispatch-n value. Expected a collection, got got:" value)
(doseq [event value] (router/dispatch event)))) (doseq [event value] (router/dispatch event)))))
;; :deregister-event-handler ;; :deregister-event-handler
@ -104,8 +104,8 @@
(fn [value] (fn [value]
(let [clear-event (partial clear-handlers events/kind)] (let [clear-event (partial clear-handlers events/kind)]
(if (sequential? value) (if (sequential? value)
(doseq [event (if (sequential? value) value [value])] (doseq [event value] (clear-event event))
(clear-event event)))))) (clear-event value)))))
;; :db ;; :db
@ -118,5 +118,6 @@
(register (register
:db :db
(fn [value] (fn [value]
(reset! app-db value))) (if-not (identical? @app-db value)
(reset! app-db value))))

View File

@ -1,8 +1,7 @@
(ns re-frame.interceptor (ns re-frame.interceptor
(:require (:require
[re-frame.loggers :refer [console]] [re-frame.loggers :refer [console]]
[re-frame.interop :refer [ratom? empty-queue debug-enabled?]] [re-frame.interop :refer [empty-queue debug-enabled?]]))
[clojure.set :as set]))
(def mandatory-interceptor-keys #{:id :after :before}) (def mandatory-interceptor-keys #{:id :after :before})

View File

@ -211,7 +211,7 @@
eddting operation. Nice and efficient, but fiddly. A bug generator eddting operation. Nice and efficient, but fiddly. A bug generator
approach. 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 every time there is ANY change. It will inspect all the todos, and
reset ALL FLAGS every time (overwriting what was there previously) reset ALL FLAGS every time (overwriting what was there previously)
and fully recalculate the list of duplicates (displayed at the bottom?). and fully recalculate the list of duplicates (displayed at the bottom?).