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
#### 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
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/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?
@ -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

View File

@ -7,3 +7,5 @@ test:
override:
- lein karma-once
- 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`
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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."
:url "https://github.com/Day8/re-frame.git"
:license {:name "MIT"}

View File

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

View File

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

View File

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