mirror of
https://github.com/status-im/re-frame.git
synced 2025-02-22 06:48:08 +00:00
WIP on the new README and initial docs
This commit is contained in:
parent
fa39d73223
commit
0638becd45
294
docs/WIP/AHighLevelCodeWalkThrough.md
Normal file
294
docs/WIP/AHighLevelCodeWalkThrough.md
Normal file
@ -0,0 +1,294 @@
|
||||
## An Initial Code Walk Through
|
||||
|
||||
Now you are armed with a high level, conceptual understanding
|
||||
of the 5 domino process, and an understanding of application state,
|
||||
we'll look at some code.
|
||||
|
||||
All 80 lines of the "simple" example application is given below,
|
||||
in domino order, heavily annotated with explanation.
|
||||
|
||||
### What this App Does
|
||||
|
||||
This app displays the time at second resolution. It also allows
|
||||
has one input field, into which you can input the colour to
|
||||
use in the display of the time.
|
||||
|
||||
XXX screenshot
|
||||
|
||||
XXX How to run it
|
||||
|
||||
### Namespace
|
||||
|
||||
Because this example is so simple, all the code is in a single namespace.
|
||||
Within it, we'll need access to both `reagent` and `re-frame`. So, we start like this:
|
||||
```clj
|
||||
(ns simple.core
|
||||
(:require [reagent.core :as reagent]
|
||||
[re-frame.core :as rf]))
|
||||
```
|
||||
|
||||
### Data Schema
|
||||
|
||||
I recommended you always write a good quality schema for your application
|
||||
state. But, here, I'm going to break my rule to keep things simple and stay
|
||||
focused on the domino code.
|
||||
|
||||
Suffice it to say the application state for "simple" looks like this
|
||||
map with two keys:
|
||||
```cljs
|
||||
{:time (js/Date.)
|
||||
:time-color "#f88"}
|
||||
```
|
||||
|
||||
Remember that re-frame holds/manages application state for you,
|
||||
supplying it to your various handlers when it is needed.
|
||||
|
||||
## Events (domino 1)
|
||||
|
||||
Events are data. You choose the format.
|
||||
|
||||
In our re-frame reference implementation we choose a vector
|
||||
format for events. For example:
|
||||
```clj
|
||||
[:delete-item 42]
|
||||
```
|
||||
|
||||
The first element in the vector identifies the kind of event. The
|
||||
further elements are optional, additional data associated with the event
|
||||
-- the additional value above (42) is presumably the id
|
||||
of the item to delete.
|
||||
|
||||
Here are some other example events:
|
||||
```clj
|
||||
[:yes-button-clicked]
|
||||
[:set-spam-wanted false :user-override "hello"]
|
||||
[:some-ns/on-success response]
|
||||
```
|
||||
|
||||
**Rule**: events are pure data. No dirty tricks like putting callback functions on the wire.
|
||||
You know who you are.
|
||||
|
||||
### dispatch
|
||||
|
||||
To send an event you call `dispatch` with the event as the argument:
|
||||
```clj
|
||||
(dispatch [:event-id value1 value2])
|
||||
```
|
||||
|
||||
In this "simple" app, a `:timer` event is sent every second:
|
||||
```clj
|
||||
(defn dispatch-timer-event
|
||||
[]
|
||||
(let [now (js/Date.)]
|
||||
(rf/dispatch [:timer now])))
|
||||
|
||||
;; call the dispatching function every second
|
||||
(defonce time-updater (js/setInterval dispatch-timer-event 1000))
|
||||
```
|
||||
This is a little unusual. Normally, it is an app's UI widgets which
|
||||
`dispatch` events (in response to user actions), or an HTTP POST's
|
||||
`on-success` handler, or a web socket which gets a new packet.
|
||||
|
||||
### After dispatch
|
||||
|
||||
When `dispatch` is passed an event vector, it just puts that
|
||||
event onto a conveyor belt for processing.
|
||||
|
||||
The event is not processes synchronously. It happens **later**.
|
||||
|
||||
The consumer on the end of the conveyor is a `router` which will organise for that
|
||||
event to be processed by the right handler.
|
||||
|
||||
|
||||
## Event Handlers (domino 2)
|
||||
|
||||
Collectively, event handlers provide the control logic in a re-frame application.
|
||||
|
||||
We must next register functions to "handle events".
|
||||
|
||||
This application has three events, identified by keywords:
|
||||
:initialise
|
||||
:time-color-change
|
||||
:timer
|
||||
|
||||
Because there's 3 events, below you'll see 3 calls to `reg-event-db`,
|
||||
each registering a handler for an event, and each occurrence like this:
|
||||
```clj
|
||||
(reg-event-db
|
||||
:id-for-the-event
|
||||
function-to-handle-the-event)
|
||||
```
|
||||
|
||||
An `event handler` registered via `reg-event-db` looks like this
|
||||
`(fn [db v] ...)`.
|
||||
It takes two parameters, the current application state `db`
|
||||
and the event vector `v` originally dispatched.
|
||||
|
||||
re-frame will ensure the right
|
||||
|
||||
An event handler must compute and return the new state of
|
||||
the application, which means it normally returns a
|
||||
modified version of `db`.
|
||||
|
||||
Below, you'll sometimes see the event handlers written:
|
||||
```clj
|
||||
(fn [db [_ something]] <-- 2nd vec param destructured to get payload value
|
||||
...)
|
||||
``
|
||||
|
||||
Note: technically, an event handler return `effects`. We're using
|
||||
`reg-event-db` here which assumes the only `effect` required (for this event)
|
||||
is a change to application state. When you need to do other effects,
|
||||
like sending emails, or http POSTing, or writing to localstore, you
|
||||
would use the more sophisticated `reg-event-fx` (note the trailing -fx) which
|
||||
is described in more advanced tutorials. We're keeping it simple for the moment.
|
||||
|
||||
```clj
|
||||
(rf/reg-event-db ;; sets up initial application state
|
||||
:initialize ;; usage: (dispatch [:initialize])
|
||||
(fn [_ _] ;; the two parameters are not important here, so use _
|
||||
{:time (js/Date.) ;; What it returns becomes the new application state
|
||||
:time-color "#f88"})) ;; so the application state will initially be a map with two keys
|
||||
|
||||
|
||||
(rf/reg-event-db ;; usage: (dispatch [:time-color-change 34562])
|
||||
:time-color-change ;; dispatched when the user enters a new colour into the UI
|
||||
(fn [db [_ new-color-value]] ;; -db event handlers given 2 parameters: current application state and event (a vector)
|
||||
(assoc db :time-color new-color-value))) ;; compute and return the new application state
|
||||
|
||||
|
||||
(rf/reg-event-db ;; usage: (dispatch [:timer a-js-Date])
|
||||
:timer ;; every second an event of this kind will be dispatched
|
||||
(fn [db [_ new-time]] ;; note how the 2nd parameter is desctructure to obtain the data value
|
||||
(assoc db :time new-time))) ;; compute and return the new application state
|
||||
```
|
||||
|
||||
## Effect Handlers (domino 3)
|
||||
|
||||
Event handlers produce `effects` which have to be actioned.
|
||||
|
||||
In this "simple" application, we're using the simplest kind of
|
||||
event handlers. They produce only one effect "please update application state".
|
||||
|
||||
This is handled for us automatically by re-frame. Nothing for us to do.
|
||||
|
||||
This is not unusual. You'll almost never have to write effect handlers, but
|
||||
we'll understand more about them in a later tutorial.
|
||||
|
||||
## Subscription Handlers (domino 4)
|
||||
|
||||
These handlers are given application state as a parameter, and they
|
||||
perform a query (computation) over it, returning a "materialised view"
|
||||
of that state.
|
||||
|
||||
These queries are used in the view functions which need to render
|
||||
DOM.
|
||||
|
||||
Now, the examples below are utterly trivial. They just extract part of the application
|
||||
state and return it. So, virtually no computation. More interesting
|
||||
subscriptions and more explanation can be found in the todomvc example.
|
||||
|
||||
`reg-sub` associates a `query identifier` with a function which computes
|
||||
that query. It's use looks like this:
|
||||
```clj
|
||||
(reg-sub
|
||||
:some-query-id ;; query identifier
|
||||
some-function) ;; the function which will compute the query
|
||||
```
|
||||
If ever a view requests data like this:
|
||||
`(listen [:some-query-id])` ;; note use of `:some-query-id`
|
||||
then `some-function` will be used to perform the query over applciation state
|
||||
when a
|
||||
|
||||
Each time application state changes, `some-function` will be
|
||||
called again to compute a new materialised view (a new computation)
|
||||
and that new value will be given to any view function which is subscribed
|
||||
to `:some-query-id`. The view function itself will then also be called again
|
||||
to create new DOM (because it depends on a query value which changed).
|
||||
|
||||
re-frame will ensure the necessary calls are made, at the right time.
|
||||
|
||||
```clj
|
||||
(rf/reg-sub
|
||||
:time
|
||||
(fn [db _] ;; db is current app state. 2nd usused param is query vector
|
||||
(:time db))) ;; return a query computation over the application state
|
||||
|
||||
(rf/reg-sub
|
||||
:time-color
|
||||
(fn [db _]
|
||||
(:time-color db)))
|
||||
```
|
||||
|
||||
|
||||
## View Functions (domino 5)
|
||||
|
||||
This is where we render the application's UI using Reagent/React.
|
||||
|
||||
As functions do, these `views` transform data into data. They source
|
||||
data from subscriptions (queries across application state), and
|
||||
the data they return is hiccup-formatted, which is a proxy for DOM.
|
||||
|
||||
Data -> HTML
|
||||
|
||||
They source data from:
|
||||
1. arguments (aka props in the React world)
|
||||
2. queries which obtain data from the application state
|
||||
|
||||
Because of the 2nd source, these functions are not pure. XXX
|
||||
|
||||
Notice that color-input below does a `dispatch`. It is very common for UI widgets
|
||||
to be event-dispatching. The user interacting with the GUI is a major source of
|
||||
events.
|
||||
|
||||
|
||||
```clj
|
||||
(defn clock
|
||||
[]
|
||||
[:div.example-clock
|
||||
{:style {:color (rf/listen [:time-color])}}
|
||||
(-> (rf/listen [:time])
|
||||
.toTimeString
|
||||
(clojure.string/split " ")
|
||||
first)])
|
||||
|
||||
(defn color-input
|
||||
[]
|
||||
[:div.color-input
|
||||
"Time color: "
|
||||
[:input {:type "text"
|
||||
:value (rf/listen [:time-color])
|
||||
:on-change #(rf/dispatch [:time-color-change (-> % .-target .-value)])}]]) ;; <---
|
||||
|
||||
(defn ui
|
||||
[]
|
||||
[:div
|
||||
[:h1 "Hello world, it is now"]
|
||||
[clock]
|
||||
[color-input]])
|
||||
```
|
||||
|
||||
|
||||
## Kick Starting The App
|
||||
|
||||
Below, `run` is the function called when the HTML loads. It kicks off the
|
||||
application.
|
||||
|
||||
It has two tasks:
|
||||
1. load the initial application state
|
||||
2. "mount" the GUI on an existing DOM element. Causes an initial render.
|
||||
|
||||
```clj
|
||||
(defn ^:export run
|
||||
[]
|
||||
(dispatch-sync [:initialize]) ;; puts a value into application state
|
||||
(reagent/render [ui] ;; mount the application's ui into '<div id="app" />'
|
||||
(js/document.getElementById "app")))
|
||||
```
|
||||
|
||||
After `run` is called, the app passively waits for events.
|
||||
Nothing happens without an `event`.
|
||||
|
||||
When it comes to establishing initial application state, you'll
|
||||
notice the use of `dispatch-sync`, rather than `dispatch`. This is
|
||||
the synchronous
|
99
docs/WIP/ApplicationState.md
Normal file
99
docs/WIP/ApplicationState.md
Normal file
@ -0,0 +1,99 @@
|
||||
## Application State
|
||||
|
||||
In our first high level code walk through, there was talk of
|
||||
"application state". It was supplied to event handlers and there was talk about
|
||||
query functions computing materialised views from it, etc.
|
||||
|
||||
Let's now look at it in more depth.
|
||||
|
||||
### On Data
|
||||
|
||||
<blockquote class="twitter-tweet" lang="en"><p>Well-formed Data at rest is as close to perfection in programming as it gets. All the crap that had to happen to put it there however...</p>— Fogus (@fogus) <a href="https://twitter.com/fogus/status/454582953067438080">April 11, 2014</a></blockquote>
|
||||
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
|
||||
### The Big Ratom
|
||||
|
||||
re-frame puts all your application state into one place, which is
|
||||
called `app-db`.
|
||||
|
||||
Ideally, you will provide a spec for this data in the one place,
|
||||
[using a powerful and leveragable schema](http://clojure.org/about/spec).
|
||||
|
||||
Now, this advice is not the slightest bit controversial for 'real' databases, right?
|
||||
You'd happily put all your well-formed data into PostgreSQL.
|
||||
|
||||
But within a running application (in memory), there is hesitation. If you have
|
||||
a background in OO, this data-in-one-place
|
||||
business is a really, really hard one to swallow. You've
|
||||
spent your life breaking systems into pieces, organised around behaviour and trying
|
||||
to hide state. I still wake up in a sweat some nights thinking about all
|
||||
that Clojure data lying around exposed and passive.
|
||||
|
||||
But, as Fogus reminded us, data at rest is perfect.
|
||||
|
||||
In re-frame's reference implementation, `app-db` is one of these:
|
||||
```clj
|
||||
(def app-db (reagent/atom {})) ;; a Reagent atom, containing a map
|
||||
```
|
||||
|
||||
Although it is a `Reagent atom` (hereafter `ratom`), I'd encourage
|
||||
you to think of it as an in-memory database. It will contain structured data.
|
||||
You will need to query that data. You will perform CRUD
|
||||
and other transformations on it. You'll often want to transact on this
|
||||
database atomically, etc. So "in-memory database"
|
||||
seems a more useful paradigm than plain old map-in-atom.
|
||||
|
||||
Further Notes:
|
||||
|
||||
1. `app-state` would probably be a more accurate name, but I choose `app-db` instead because
|
||||
I wanted to convey the database notion as strongly as possible.
|
||||
2. In the documentation and code, I make a distinction between `app-db` (the `ratom`) and
|
||||
`db` which is the (map) `value` currently stored **inside** this `ratom`.
|
||||
3. the reference implementation creates and manages an `app-db` for you, so
|
||||
you don't need to declare one yourself (see the 1st FAQ if you want to inspect the value it holds).
|
||||
4. `app-db` doesn't actually have to be a `ratom` containing a map. It could, for example,
|
||||
be a [datascript](https://github.com/tonsky/datascript database). In fact, any database which
|
||||
can signal you when it changes would do. We'd love! to be using [datascript](https://github.com/tonsky/datascript database) - so damn cool -
|
||||
but we had too much data in our apps. If you were to use it, you'd have to tweak the
|
||||
reference implementation a bit, [perhaps using this inspiration](https://gist.github.com/allgress/11348685).
|
||||
|
||||
|
||||
### The Benefits Of Data-In-The-One-Place
|
||||
|
||||
1. Here's the big one: because there is a single source of truth, we write no
|
||||
code to synchronize state between many different stateful components. I
|
||||
cannot stress too much how significant this is. You end up writing less code
|
||||
and an entire class of bugs is eliminated.
|
||||
(This mindset very different to OO which involves
|
||||
distributing state across objects, and then ensuring that state is synchronized, all the while
|
||||
trying to hide it, which is, when you think about it, quite crazy ... and I did it for years).
|
||||
|
||||
2. Because all app state is coalesced into one atom, it can be updated
|
||||
with a single `reset!`, which acts like a transactional commit. There is
|
||||
an instant in which the app goes from one state to the next, never a series
|
||||
of incremental steps which can leave the app in a temporarily inconsistent, intermediate state.
|
||||
Again, this simplicity causes a certain class of bugs or design problems evaporate.
|
||||
|
||||
3. The data in `app-db` can be given a strong schema
|
||||
so that, at any moment, we can validate all the data in the application. **All of it.**
|
||||
We do this check after every single "event handler" runs (event handlers compute new state).
|
||||
And this enables us to catch errors early (and accurately). It increases confidence in the way
|
||||
that Types can increase confidence, only [a good schema can provide more
|
||||
**leverage** than types](https://www.youtube.com/watch?v=nqY4nUMfus8).
|
||||
|
||||
4. Undo/Redo [becomes straight forward to implement](https://github.com/Day8/re-frame-undo).
|
||||
It is easy to snapshot and restore one central value. Immutable data structures have a
|
||||
feature called `structural sharing` which means it doesn't cost much RAM to keep the last, say, 200
|
||||
snapshots. All very efficient.
|
||||
For certain categories of applications (eg: drawing applications) this feature is borderline magic.
|
||||
Instead of undo/redo being hard, disruptive and error prone, it becomes virtually trivial.
|
||||
**But,** many web applications are not self contained
|
||||
data-wise and, instead, are dominated by data sourced from an authoritative remote database.
|
||||
For these applications, re-frame's `app-db` is mostly a local caching
|
||||
point, and being able to do undo/redo its state is meaningless because the authoritative
|
||||
source of data is elsewhere.
|
||||
|
||||
5. The ability to genuinely model control via FSMs (discussed later)
|
||||
|
||||
6. The ability to do time travel debugging, even in a production setting. More soon.
|
||||
|
0
docs/WIP/Mental-Models.md
Normal file
0
docs/WIP/Mental-Models.md
Normal file
@ -1,145 +1,4 @@
|
||||
[logo](/images/logo/re-frame_512w.png?raw=true)
|
||||
|
||||
## Derived Values, Flowing
|
||||
|
||||
> This, milord, is my family's axe. We have owned it for almost nine hundred years, see. Of course,
|
||||
sometimes it needed a new blade. And sometimes it has required a new handle, new designs on the
|
||||
metalwork, a little refreshing of the ornamentation . . . but is this not the nine hundred-year-old
|
||||
axe of my family? And because it has changed gently over time, it is still a pretty good axe,
|
||||
y'know. Pretty good.
|
||||
|
||||
> -- Terry Pratchett, The Fifth Elephant <br>
|
||||
> Reflecting on time, identity, flow and derived values
|
||||
|
||||
## Why Should You Care?
|
||||
|
||||
Perhaps:
|
||||
|
||||
1. You want to develop an [SPA] in ClojureScript, and you are looking for a framework
|
||||
2. You believe that Facebook did something magnificent when it created React, and
|
||||
you are curious about the further implications. Is the combination of
|
||||
`reactive programming`, `functional programming` and `immutable data` going to
|
||||
**completely change everything**? And, if so, what would that look like in a language
|
||||
that embraces those paradigms?
|
||||
3. You're taking a [Functional Design and Programming course at San Diego State University](http://www.eli.sdsu.edu/courses/fall15/cs696/index.html)
|
||||
and you have to learn re-frame to do an assignment. You've left it a bit late, right?
|
||||
Good news, there is a quick start guide shortly.
|
||||
4. You demand social proof in your frameworks!!! Luckily, 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
|
||||
xkcd reference (soon) and a hilarious, insiders-joke T-shirt,
|
||||
ideal for conferences (in design). What could possibly go wrong?
|
||||
|
||||
## re-frame
|
||||
|
||||
re-frame is a pattern for writing [SPAs] in ClojureScript, using [Reagent].
|
||||
|
||||
This repo contains both a **description of this pattern** and
|
||||
a **reference implementation**.
|
||||
|
||||
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**."
|
||||
|
||||
Because it is a functional framework, you program it by:
|
||||
- designing data and
|
||||
- writing pure functions which transform this data
|
||||
|
||||
### It is a loop
|
||||
|
||||
Architecturally, re-frame implements "a perpetual loop".
|
||||
|
||||
To build an app, you hang functions on certain parts of this loop,
|
||||
and re-frame looks after the `conveyance of data` (flow of data)
|
||||
around the loop, into and out of the transforming functions you
|
||||
provide - hence the tag line "Derived Data, Flowing".
|
||||
|
||||
### With 5 dominoes
|
||||
|
||||
Computationally, each iteration of the loop involves the same
|
||||
5 domino cascade. One domino triggering the next, which triggers the next, etc,
|
||||
until we are back at the beginning of the loop. Each iteration has the same cascade.
|
||||
|
||||
An `event` acts as the **1st domino**.
|
||||
|
||||
An event might be user initiated,
|
||||
like "delete button clicked", or it might be initiated by another outside agent, like "a websocket
|
||||
delivered a document". Without the impulse of a triggering `event`, no 5 domino cascade occurs.
|
||||
So, it is only because of `events` that a re-frame app is propelled, loop iteration after loop iteration,
|
||||
from one state to the next.
|
||||
|
||||
re-frame is `event` driven.
|
||||
|
||||
The **2nd domino** is `event handling` which involves computing how the application should
|
||||
respond/change to the new `event` occurrence.
|
||||
|
||||
Event handlers produce `effects` or, more accurately,
|
||||
a **description** of `effects`. These descriptions say how the state of
|
||||
an SPA itself should change, and sometimes they also say how the outside world should change
|
||||
(localstore, cookies, databases, emails, etc).
|
||||
|
||||
The **3rd domino** takes these descriptions (of `effects`) and actions them. Makes them real.
|
||||
|
||||
Now, to a functional programmer, `effects` are scary, in a [xenomorph kind of way](https://www.google.com.au/search?q=xenomorph).
|
||||
Nothing messes with functional purity
|
||||
quite like the need for effects and coeffects. But, on the other hand, `effects` are equally
|
||||
marvelous because they take the app forward. Without them, an app stays stuck in one state forever,
|
||||
never achieving anything.
|
||||
|
||||
So re-frame embraces the protagonist nature of `effects` - the entire, unruly zoo of them - but
|
||||
it does so in a controlled, debuggable, auditable, mockable, plugable way.
|
||||
|
||||
After the effectful 3rd domino handlers have run,
|
||||
something about the world will have changed, often the app's state. **Dominoes 4 and 5** close
|
||||
the re-frame loop by re-rendering the UI to reflect any change in application state.
|
||||
|
||||
These two dominoes combine to implement the formula made famous by React: `v = f(s)` - a view `v`
|
||||
is a function `f` of the app state `s`. **Over time**, when `s` changes, `f`
|
||||
must be called again to compute new `v`.
|
||||
|
||||
Given domino 3 changed application state, `s`, dominos 4 and 5 must conceptually re-run `f` so as
|
||||
to produce a new `v` which shows this change.
|
||||
|
||||
The mechanics of this are as follows (do not be alarmed by the terminology): domino 4 is a
|
||||
de-duplicated signal graph which reactivity queries
|
||||
application state, while domino 5 involves the reactive, declarative rendering
|
||||
of views using React/Reagent. You'll see nice simple code in a minute.
|
||||
|
||||
### A Dominoes Walk Through
|
||||
|
||||
Imagine the following scenario: the user clicks the delete button
|
||||
for the 3rd item in a list. In response, what happens within a re-frame app?
|
||||
|
||||
The 5 domino cascade:
|
||||
|
||||
1. The click handler for that button uses the re-frame supplied function, `dispatch`,
|
||||
to send an `event`, which might look like this `[:delete-item 2]`. Yes, that's a vector of two elements.
|
||||
2. The `event handler` (function) associated with `:delete-item` (the first element of the event)
|
||||
is called to compute what the `effect` of the `event` should be. In this case, it computes
|
||||
that new application state should result (this state will not include the deleted item).
|
||||
3. an `effect handler` (function) resets application state to the newly computed value.
|
||||
4. a query (function) over the application state is called (reactively), computing a new
|
||||
result (containing no 3rd item!).
|
||||
5. a view (function) is called to re-compute DOM (reactively), because the query state to which
|
||||
it is subscribed has changed. (Thereafter, React invisibly does the dirty work
|
||||
of actually mutating the DOM).
|
||||
|
||||
At this point, the re-frame app returns to a quiescent state, waiting for the next event. When it comes, a
|
||||
similar 5 domino cascade will happen again.
|
||||
|
||||
### A Simple Loop Of Simple Functions
|
||||
|
||||
**Each of these dominoes are simple, pure functions** which can be be described, understood and
|
||||
tested independently (other than domino 3). They take data, transform it and return new data.
|
||||
|
||||
The loop itself is utterly predictable and 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 a great ease in reasoning and debugging.
|
||||
|
||||
**At this point you know 50% of re-frame.** We still have to fill in some detail, of course,
|
||||
but the core concepts are known to you.
|
||||
|
||||
### Some Code
|
||||
|
||||
@ -156,34 +15,6 @@ but the core concepts are known to you.
|
||||
At this point you need to see some code ... XXXXXX
|
||||
|
||||
|
||||
## Data Oriented Design
|
||||
|
||||
You might already know that ClojureScript is a modern lisp, and that
|
||||
lisps are **homoiconic**. If not, you do now.
|
||||
|
||||
The homoiconic bit is significant. It means you program in a lisp by creating and
|
||||
assembling lisp data structures. So you are **programming in data**.
|
||||
The functions which later manipulate data, start as data.
|
||||
|
||||
Clojure programmers place particular
|
||||
emphasis on the primacy of data. When they aren't re-watching Rich Hickey videos,
|
||||
and wishing their hair was darker and more curly,
|
||||
they meditate on aphorisms like "Data is the ultimate in late binding"
|
||||
and "data > functions > macros".
|
||||
|
||||
I cannot stress too much what a big deal this is. It might seem
|
||||
just a syntax curiosity at first but, when the penny drops for
|
||||
you on this, it tends to be a profound moment. And once you
|
||||
understand the importance of this concept at the language level,
|
||||
you naturally want a similar approach at the library level.
|
||||
|
||||
So, it will come as no surprise, then, to know that re-frame has a
|
||||
data oriented design. Events are data. Effects are data. DOM is data.
|
||||
The functions which transform data are registered and looked up via
|
||||
data. Interceptors (data) are preferred over middleware (higher
|
||||
order functions). Etc.
|
||||
|
||||
Data - that's the way we roll.
|
||||
|
||||
## Finite State Machines
|
||||
|
||||
@ -214,24 +45,6 @@ So, in this section, I'm suggesting that this perspective is useful sometimes:
|
||||
|
||||
Events - that's the way we roll.
|
||||
|
||||
### It is Mature
|
||||
|
||||
re-frame was released early 2015, and it has subsequently been
|
||||
used by a number of companies and individuals to build complex apps.
|
||||
|
||||
Frameworks
|
||||
are just pesky overhead at small scale - measure them instead by how they help
|
||||
you tame the complexity of bigger apps, and in this regard re-frame seems to have
|
||||
worked out well. Some have even praised it effusively.
|
||||
|
||||
Having said that, re-frame remains a work in progress and it falls
|
||||
short in a couple of ways - for example it doesn't work as well as we
|
||||
want with devcards - we're still
|
||||
puzzling over those aspects and we continue tweaking as we go.
|
||||
|
||||
And, yes, it is fast, straight out of the box. And, yes, it has
|
||||
a good testing story. And, yes, it works in with figwheel to create
|
||||
a delightful live-coding development story.
|
||||
|
||||
|
||||
## It Does Physics
|
||||
@ -255,14 +68,6 @@ this diagram, apart from being a plausible analogy, and bordering on useful,
|
||||
is practically PROOF that re-frame is doing physics.
|
||||
|
||||
|
||||
[](https://clojars.org/re-frame)
|
||||
[](license.txt)
|
||||
[](https://circleci.com/gh/Day8/re-frame/tree/develop)
|
||||
[](https://circleci.com/gh/Day8/re-frame/tree/master)
|
||||
[](https://github.com/Day8/re-frame/tree/master/examples)
|
||||
|
||||
|
||||
__Warning__: That was the summary. What follows is a long-ish tutorial/explanation.
|
||||
|
||||
## Tutorial Table of Contents
|
||||
|
||||
@ -364,112 +169,10 @@ introductory Reagent tutorials before going on. Try:
|
||||
- [Building Single Page Apps with Reagent](http://yogthos.net/posts/2014-07-15-Building-Single-Page-Apps-with-Reagent.html).
|
||||
|
||||
|
||||
## Explaining re-frame
|
||||
|
||||
To explain re-frame, I'll incrementally develop a diagram, describing each part as it is added.
|
||||
|
||||
### On Data
|
||||
|
||||
<blockquote class="twitter-tweet" lang="en"><p>Well-formed Data at rest is as close to perfection in programming as it gets. All the crap that had to happen to put it there however...</p>— Fogus (@fogus) <a href="https://twitter.com/fogus/status/454582953067438080">April 11, 2014</a></blockquote>
|
||||
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
|
||||
### The Big Ratom
|
||||
|
||||
Our re-frame diagram starts (very modestly) with Fogus' ***well-formed data at rest*** bit:
|
||||
```
|
||||
app-db
|
||||
```
|
||||
|
||||
re-frame says to put all your application state into one place, which we'll
|
||||
call `app-db`. Ideally, you will also provide a spec for this data in the one place,
|
||||
[using a powerful and leveragable schema](http://clojure.org/about/spec).
|
||||
|
||||
Now, this advice is not the slightest bit controversial for 'real' databases, right?
|
||||
You'd happily put all your well-formed data into PostgreSQL.
|
||||
|
||||
But within a running application (in memory), there is hesitation. If you have
|
||||
a background in OO, this data-in-one-place
|
||||
business is a really, really hard one to swallow. You've
|
||||
spent your life breaking systems into pieces, organised around behaviour and trying
|
||||
to hide state. I still wake up in a sweat some nights thinking about all
|
||||
that Clojure data lying around exposed and passive.
|
||||
|
||||
But, as Fogus reminded us, data at rest is perfect.
|
||||
|
||||
From here on in this document, we'll assume `app-db` is one of these:
|
||||
```clj
|
||||
(def app-db (reagent/atom {})) ;; a Reagent atom, containing a map
|
||||
```
|
||||
|
||||
Although it is a `Reagent atom` (hereafter `ratom`), I'd encourage
|
||||
you to think of it as an in-memory database. It will contain structured data.
|
||||
You will need to query that data. You will perform CRUD
|
||||
and other transformations on it. You'll often want to transact on this
|
||||
database atomically, etc. So "in-memory database"
|
||||
seems a more useful paradigm than plain old map-in-atom.
|
||||
|
||||
Further Notes:
|
||||
|
||||
1. `app-state` would probably be a more accurate name, but I choose `app-db` instead because
|
||||
I wanted to convey the database notion as strongly as possible.
|
||||
2. In the documentation and code I make the distinction between `app-db` (the `ratom`) and
|
||||
`db` which is the (map) `value` currently stored **inside** the `ratom`. It will help with
|
||||
3. the reference implementation creates and manages an `app-db` for you, so
|
||||
you don't need to declare one yourself (see the 1st FAQ if you want to inspect the values in it).
|
||||
your reading of the material if you keep this distinction in mind as we go forward.
|
||||
4. `app-db` doesn't actually have to be a `ratom` containing a map. It could, for example,
|
||||
be a [datascript] database. In fact, any database which is reactive
|
||||
(can tell you when it changes) would do. We'd love! to be using [datascript] - so damn cool -
|
||||
but we had too much data in our apps. If you were to use it, you'd have to tweak the
|
||||
reference implementation a bit, [perhaps using this inspiration](https://gist.github.com/allgress/11348685).
|
||||
|
||||
|
||||
### The Benefits Of Data-In-The-One-Place
|
||||
|
||||
1. Here's the big one: because there is a single source of truth, we write no
|
||||
code to synchronizes state between many different stateful components. I
|
||||
cannot stress too much how significant this is. You end up writing less code
|
||||
and an entire class of bugs is eliminated. The simplicity it brings is breathtaking.
|
||||
(This mindset very different to OO which involves
|
||||
distributing state across objects, and then ensuring that state is synchronized, all the while
|
||||
trying to hide it, which is, when you think about it, quite crazy ... and I did it for years).
|
||||
|
||||
2. Because all app state is coalesced into one value, it can be updated
|
||||
with a single `reset!` operation, which acts like a transactional commit. There is
|
||||
an instant in which the app goes from one state to the next, never a series
|
||||
of incremental steps which can leave the app in a temporarily inconsistent, intermediate state.
|
||||
Again, this simplicity caused a certain class of bugs or design problems evaporate.
|
||||
|
||||
3. The data in `app-db` can be given a strong schema
|
||||
so that, at any moment, we can validate all the data in the application. **All of it.**
|
||||
We do this check after every single "event" is processed (events are what "change" state).
|
||||
And this enables us to catch errors early (and accurately). It increases confidence in the way
|
||||
that Types can increase confidence, only [a good schema can provide more
|
||||
**leverage** than types](https://www.youtube.com/watch?v=nqY4nUMfus8).
|
||||
|
||||
4. Undo/Redo [becomes straight forward to implement](https://github.com/Day8/re-frame-undo).
|
||||
It is easy to snapshot and restore one central value. Immutable data structures have a
|
||||
feature called `structural sharing` which means it doesn't cost much RAM to keep the last, say, 200
|
||||
snapshots. All very efficient.
|
||||
For certain categories of applications (eg: drawing applications) this feature is borderline magic.
|
||||
Instead of undo/redo being hard, disruptive and error prone, it becomes virtually trivial.
|
||||
**But,** many web applications are not self contained
|
||||
data-wise and, instead, are dominated by data sourced from an authoritative remote database.
|
||||
For these applications, re-frame's `app-db` is mostly a local caching
|
||||
point, and being able to do undo/redo its state is meaningless because the authoritative
|
||||
source of data is elsewhere.
|
||||
|
||||
5. The ability to genuinely model control via FSMs (discussed later)
|
||||
|
||||
6. The ability to do time travel debugging, even in a production setting. More soon.
|
||||
|
||||
## Flow
|
||||
|
||||
Back in the introduction, I explained that re-frame implements a loop and that each iteration involves a
|
||||
5-domino-cascade.
|
||||
|
||||
Now we're discussed We're going to discuss dominoes 4 and 5.
|
||||
|
||||
Arguments from authority ...
|
||||
|
||||
> Everything flows, nothing stands still. (Panta rhei)
|
||||
@ -1070,27 +773,6 @@ could almost imagine them as a "stored procedures" on a
|
||||
database. Almost. Stretching it? We do like our in-memory
|
||||
database analogies.
|
||||
|
||||
### What are events?
|
||||
|
||||
Events are data. You choose the format.
|
||||
|
||||
In our reference implementation we choose a vector format. For example:
|
||||
|
||||
[:delete-item 42]
|
||||
|
||||
The first item in the vector identifies the event and
|
||||
the rest of the vector is the optional parameters -- in the example above, the id (42) of the item to delete.
|
||||
|
||||
Here are some other example events:
|
||||
|
||||
```Clojure
|
||||
[:yes-button-clicked]
|
||||
[:set-spam-wanted false]
|
||||
[[:complicated :multi :part :key] "a parameter" "another one" 45.6]
|
||||
```
|
||||
|
||||
**Rule**: events are pure data. No dirty tricks like putting callback functions on the wire.
|
||||
You know who you are.
|
||||
|
||||
### Dispatching Events
|
||||
|
||||
@ -1162,11 +844,6 @@ issues like Middleware [in the Wiki](https://github.com/Day8/re-frame/wiki#handl
|
||||
|
||||
### Routing
|
||||
|
||||
When `dispatch` is passed an event vector, it just puts that event onto a conveyor belt.
|
||||
|
||||
The consumer on the end of the conveyor is a `router` which will organise for that
|
||||
event to be processed by the right handler.
|
||||
|
||||
|
||||
```
|
||||
app-db --> components --> Hiccup --> Reagent --> VDOM --> React --> DOM
|
||||
@ -1317,39 +994,7 @@ This is utterly, utterly perfect for debugging assuming, of course, you are in a
|
||||
a checkpoint, and the events since then.
|
||||
|
||||
|
||||
### In Summary
|
||||
|
||||
re-frame has two distinct flows, and I claim they are BOTH FRP in nature. The first is clearly FRP.
|
||||
The second one is conceptually FRP, but you do have to squint.
|
||||
|
||||
All the parts are simple. The parts are easy to understand in isolation. The parts are composed so that
|
||||
derived data flows in a perpetual reactive loop, through pure functions.
|
||||
|
||||
To build an app using re-frame, you'll have to:
|
||||
- design your app's data structure.
|
||||
- write and register subscription functions (query layer).
|
||||
- write component functions (view layer).
|
||||
- write and register event handler functions (control layer and/or state transition layer).
|
||||
|
||||
|
||||
### Where Do I Go Next?
|
||||
|
||||
Next:
|
||||
- look at the examples: https://github.com/Day8/re-frame/tree/master/examples
|
||||
- read the docs: https://github.com/Day8/re-frame/blob/master/docs/README.md
|
||||
|
||||
Then:
|
||||
- use the lein template: https://github.com/Day8/re-frame-template
|
||||
|
||||
Also, if you want reusable layout and widget components, consider this sister project:
|
||||
https://github.com/Day8/re-com
|
||||
|
||||
|
||||
### Licence
|
||||
|
||||
Copyright © 2015 Michael Thompson
|
||||
|
||||
Distributed under The MIT License (MIT) - See LICENSE.txt
|
||||
|
||||
[SPAs]:http://en.wikipedia.org/wiki/Single-page_application
|
||||
[SPA]:http://en.wikipedia.org/wiki/Single-page_application
|
||||
@ -1361,6 +1006,5 @@ Distributed under The MIT License (MIT) - See LICENSE.txt
|
||||
[Elm]:http://elm-lang.org/
|
||||
[OM]:https://github.com/swannodette/om
|
||||
[Prismatic Schema]:https://github.com/Prismatic/schema
|
||||
[datascript]:https://github.com/tonsky/datascript
|
||||
[Hoplon]:http://hoplon.io/
|
||||
[Pedestal App]:https://github.com/pedestal/pedestal-app
|
224
docs/WIP/README.md
Normal file
224
docs/WIP/README.md
Normal file
@ -0,0 +1,224 @@
|
||||
[logo](/images/logo/re-frame_128w.png?raw=true)
|
||||
|
||||
## Derived Values, Flowing
|
||||
|
||||
> This, milord, is my family's axe. We have owned it for almost nine hundred years, see. Of course,
|
||||
sometimes it needed a new blade. And sometimes it has required a new handle, new designs on the
|
||||
metalwork, a little refreshing of the ornamentation ... but is this not the nine hundred-year-old
|
||||
axe of my family? And because it has changed gently over time, it is still a pretty good axe,
|
||||
y'know. Pretty good.
|
||||
|
||||
> -- Terry Pratchett, The Fifth Elephant <br>
|
||||
> Reflecting on identity, flow and derived values
|
||||
|
||||
|
||||
[](https://clojars.org/re-frame)
|
||||
[](license.txt)
|
||||
[](https://circleci.com/gh/Day8/re-frame/tree/develop)
|
||||
[](https://circleci.com/gh/Day8/re-frame/tree/master)
|
||||
|
||||
|
||||
## Why Should You Care?
|
||||
|
||||
Perhaps:
|
||||
|
||||
1. You want to develop an [SPA] in ClojureScript, and you are looking for a framework
|
||||
2. You believe that Facebook did something magnificent when it created React, and
|
||||
you are curious about the further implications. Is the combination of
|
||||
`reactive programming`, `functional programming` and `immutable data` going to
|
||||
**completely change everything**? And, if so, what would that look like in a language
|
||||
that embraces those paradigms?
|
||||
3. You're taking a [Functional Design and Programming course at San Diego State University](http://www.eli.sdsu.edu/courses/fall15/cs696/index.html)
|
||||
and you have to learn re-frame to do an assignment. You've left it a bit late, right?
|
||||
Good news, there is a quick start guide shortly.
|
||||
4. You want your framework to exude social proof!! Luckily, 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
|
||||
xkcd reference (soon) and a hilarious, insiders-joke T-shirt,
|
||||
ideal for conferences (in design). What could possibly go wrong?
|
||||
|
||||
## re-frame
|
||||
|
||||
re-frame is a pattern for writing [SPAs] in ClojureScript, using [Reagent].
|
||||
|
||||
This repo contains both a **description of this pattern** and
|
||||
a **reference implementation**.
|
||||
|
||||
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, you program it by:
|
||||
- designing data and
|
||||
- writing pure functions which transform this data
|
||||
|
||||
### It Is A Loop
|
||||
|
||||
Architecturally, re-frame implements "a perpetual loop".
|
||||
|
||||
To build an app, you hang pure functions on certain parts of this loop,
|
||||
and re-frame looks after the `conveyance of data` (flow of data)
|
||||
around the loop, into and out of the transforming functions you
|
||||
provide - hence the tag line "Derived Data, Flowing".
|
||||
|
||||
### It Has 5 Dominoes
|
||||
|
||||
Computationally, each iteration of the loop involves the same
|
||||
5 domino cascade. One domino triggering the next, which triggers the next, etc,
|
||||
until we are back at the beginning of the loop. Each iteration has the same cascade.
|
||||
|
||||
<img style="float: right;" src="/images/readme/dominos.jpg">
|
||||
|
||||
An `event` acts as the **1st domino**.
|
||||
|
||||
An event might be initiated by a user clicking a button, or entering a field,
|
||||
or it might be initiated by another agent, like "a websocket receiving a packet".
|
||||
Without the impulse of a triggering `event`, no 5 domino cascade occurs.
|
||||
So, it is only because of `events` that a re-frame app is propelled, loop iteration after loop iteration,
|
||||
from one state to the next.
|
||||
|
||||
re-frame is `event` driven.
|
||||
|
||||
The **2nd domino**, `event handling`, involves computing how the application should
|
||||
respond/change to the new `event` occurrence.
|
||||
|
||||
Event handlers produce `effects` or, more accurately,
|
||||
a **description** of `effects`. These descriptions say how the state of
|
||||
an SPA itself should change, and sometimes they also say how the outside world should change
|
||||
(localstore, cookies, databases, emails, etc).
|
||||
|
||||
The **3rd domino** takes these descriptions (of `effects`) and actions them. Makes them real.
|
||||
|
||||
Now, to a functional programmer, `effects` are scary, in a [xenomorph kind of way](https://www.google.com.au/search?q=xenomorph).
|
||||
Nothing messes with functional purity
|
||||
quite like the need for effects and coeffects. But, on the other hand, `effects` are equally
|
||||
marvelous because they take the app forward. Without them, an app stays stuck in one state forever,
|
||||
never achieving anything.
|
||||
|
||||
So re-frame embraces the protagonist nature of `effects` - the entire, unruly zoo of them - but
|
||||
it does so in a controlled, debuggable, auditable, mockable, plugable way.
|
||||
|
||||
After the effectful 3rd domino handlers have run,
|
||||
something about the world will have changed, often the app's state. **Dominoes 4 and 5** close
|
||||
the re-frame loop by re-rendering the UI to reflect any change in application state.
|
||||
|
||||
These two dominoes combine to implement the formula made famous by React: `v = f(s)` - a view `v`
|
||||
is a function `f` of the app state `s`. **Over time**, when `s` changes, `f`
|
||||
will be called again to compute new `v`, forever keeping `v` up to date with the current `s`.
|
||||
|
||||
Given domino 3 changed application state, `s`, dominos 4 and 5 are about re-running `f` so as
|
||||
to produce a new `v` which shows this change.
|
||||
|
||||
The mechanics of this are as follows (do not be alarmed by the terminology): domino 4 is a
|
||||
de-duplicated signal graph which reactivity queries
|
||||
application state, while domino 5 involves the reactive, declarative rendering
|
||||
of views using React/Reagent. You'll see nice simple code in a minute.
|
||||
|
||||
### A Dominoes Walk Through
|
||||
|
||||
Imagine the following scenario: the user clicks the delete button
|
||||
for the 3rd item in a list. In response, what happens within a re-frame app?
|
||||
|
||||
The 5 domino cascade:
|
||||
|
||||
1. The click handler for that button uses the re-frame supplied function, `dispatch`,
|
||||
to send an `event`, which might look like this `[:delete-item 2]`. Yes, that's a vector of two elements.
|
||||
2. The `event handler` (function) associated with `:delete-item` (the first element of the event)
|
||||
is called to compute what the `effect` of the `event` should be. In this case, it computes
|
||||
that new application state should result (this state will not include the deleted item).
|
||||
3. an `effect handler` (function) resets application state to the newly computed value.
|
||||
4. a query (function) over the application state is called (reactively), computing a new
|
||||
result (containing no 3rd item!).
|
||||
5. a view (function) is called to re-compute DOM (reactively), because the query state to which
|
||||
it is subscribed has changed.
|
||||
|
||||
At this point, the re-frame app returns to a quiescent state, waiting for the next event. When one comes, a
|
||||
similar 5 domino cascade will happen again.
|
||||
|
||||
### A Simple Loop Of Simple Functions
|
||||
|
||||
**Each of these dominoes are simple, pure functions** which can be be described, understood and
|
||||
tested independently (other than domino 3). They take data, transform it and return new data.
|
||||
|
||||
The loop itself is utterly predictable and 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 a great ease in reasoning and debugging.
|
||||
|
||||
**At this point you know 50% of re-frame.** Sure, there's some detail to fill in,
|
||||
but the core concepts are known to you.
|
||||
|
||||
## It Leverages Data
|
||||
|
||||
You might already know that ClojureScript is a modern lisp, and that
|
||||
lisps are **homoiconic**. If not, you do now.
|
||||
|
||||
The homoiconic bit is significant. It means you program in a lisp by creating and
|
||||
assembling lisp data structures. So you are **programming in data**.
|
||||
The functions which later manipulate data, start as data.
|
||||
|
||||
Clojure programmers place particular
|
||||
emphasis on the primacy of data. When they aren't re-watching Rich Hickey videos,
|
||||
and wishing their hair was darker and more curly,
|
||||
they meditate on aphorisms like "Data is the ultimate in late binding"
|
||||
and "data > functions > macros".
|
||||
|
||||
I cannot stress too much what a big deal this is. It might seem
|
||||
just a syntax curiosity at first but, when the penny drops for
|
||||
you on this, it tends to be a profound moment. And once you
|
||||
understand the importance of this concept at the language level,
|
||||
you naturally want a similar approach at the library level.
|
||||
|
||||
So, it will come as no surprise, then, to know that re-frame has a
|
||||
data oriented design. Events are data. Effects are data. DOM is data.
|
||||
The functions which transform data are registered and looked up via
|
||||
data. Interceptors (data) are preferred over middleware (higher
|
||||
order functions). Etc.
|
||||
|
||||
Data - that's the way we roll.
|
||||
|
||||
### It is Mature
|
||||
|
||||
re-frame was released early 2015, and it has subsequently been
|
||||
used by a number of companies and individuals to build complex apps.
|
||||
|
||||
Frameworks
|
||||
are just pesky overhead at small scale - measure them instead by how they help
|
||||
you tame the complexity of bigger apps, and in this regard re-frame seems to have
|
||||
worked out well. Some have even praised it effusively.
|
||||
|
||||
Having said that, re-frame remains a work in progress and it falls
|
||||
short in a couple of ways - for example it doesn't work as well as we
|
||||
want with devcards - we're still
|
||||
puzzling over those aspects and we continue tweaking as we go.
|
||||
|
||||
And, yes, it is fast, straight out of the box. And, yes, it has
|
||||
a good testing story. And, yes, it works in with figwheel to create
|
||||
a delightful live-coding development story.
|
||||
|
||||
### Where Do I Go Next?
|
||||
|
||||
Want to see code? Want more mental models? Read the docs:
|
||||
https://github.com/Day8/re-frame/blob/master/docs/README.md
|
||||
|
||||
Look at the examples:
|
||||
https://github.com/Day8/re-frame/tree/master/examples
|
||||
|
||||
Use a template to create your own project:
|
||||
https://github.com/Day8/re-frame-template
|
||||
|
||||
Use these resources:
|
||||
https://github.com/Day8/re-com
|
||||
XXX
|
||||
|
||||
### Licence
|
||||
|
||||
Copyright © 2015 Michael Thompson
|
||||
|
||||
Distributed under The MIT License (MIT) - See LICENSE.txt
|
||||
|
||||
|
||||
[SPAs]:http://en.wikipedia.org/wiki/Single-page_application
|
||||
[SPA]:http://en.wikipedia.org/wiki/Single-page_application
|
||||
[Reagent]:http://reagent-project.github.io/
|
@ -12,7 +12,7 @@
|
||||
<script src="js/client.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
simpleexample.core.run();
|
||||
simple.core.run();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
83
examples/simple/src/simple/core.cljs
Normal file
83
examples/simple/src/simple/core.cljs
Normal file
@ -0,0 +1,83 @@
|
||||
(ns simple.core
|
||||
(:require [reagent.core :as reagent]
|
||||
[re-frame.core :as rf]))
|
||||
|
||||
;; -- Domino 1 - Event Dispatch -----------------------------------------------
|
||||
|
||||
(defn dispatch-timer-event
|
||||
[]
|
||||
(let [now (js/Date.)]
|
||||
(rf/dispatch [:timer now])))
|
||||
|
||||
;; an event will be dispatched every second
|
||||
(defonce time-updater (js/setInterval dispatch-timer-event 1000))
|
||||
|
||||
|
||||
;; -- Domino 2 - Event Handlers -----------------------------------------------
|
||||
|
||||
(rf/reg-event-db ;; sets up initial application state
|
||||
:initialize ;; usage: (dispatch [:initialize])
|
||||
(fn [_ _] ;; the two parameters are not important here, so use _
|
||||
{:time (js/Date.) ;; What it returns becomes the new application state
|
||||
:time-color "#f88"})) ;; so the application state will initially be a map with two keys
|
||||
|
||||
|
||||
(rf/reg-event-db ;; usage: (dispatch [:time-color-change 34562])
|
||||
:time-color-change ;; dispatched when the user enters a new colour into the UI
|
||||
(fn [db [_ new-color-value]] ;; -db event handlers given 2 parameters: current application state and event (a vector)
|
||||
(assoc db :time-color new-color-value))) ;; compute and return the new application state
|
||||
|
||||
|
||||
(rf/reg-event-db ;; usage: (dispatch [:timer a-js-Date])
|
||||
:timer ;; every second an event of this kind will be dispatched
|
||||
(fn [db [_ new-time]] ;; note how the 2nd parameter is desctructure to obtain the data value
|
||||
(assoc db :time new-time))) ;; compute and return the new application state
|
||||
|
||||
|
||||
;; -- Domino 4 - Query -------------------------------------------------------
|
||||
|
||||
(rf/reg-sub
|
||||
:time
|
||||
(fn [db _] ;; db is current app state. 2nd usused param is query vector
|
||||
(:time db))) ;; return a query computation over the application state
|
||||
|
||||
(rf/reg-sub
|
||||
:time-color
|
||||
(fn [db _]
|
||||
(:time-color db)))
|
||||
|
||||
|
||||
;; -- Domino 5 - View Functions ----------------------------------------------
|
||||
|
||||
(defn clock
|
||||
[]
|
||||
[:div.example-clock
|
||||
{:style {:color (rf/listen [:time-color])}}
|
||||
(-> (rf/listen [:time])
|
||||
.toTimeString
|
||||
(clojure.string/split " ")
|
||||
first)])
|
||||
|
||||
(defn color-input
|
||||
[]
|
||||
[:div.color-input
|
||||
"Time color: "
|
||||
[:input {:type "text"
|
||||
:value (rf/listen [:time-color])
|
||||
:on-change #(rf/dispatch [:time-color-change (-> % .-target .-value)])}]]) ;; <---
|
||||
|
||||
(defn ui
|
||||
[]
|
||||
[:div
|
||||
[:h1 "Hello world, it is now"]
|
||||
[clock]
|
||||
[color-input]])
|
||||
|
||||
;; -- Entry Point -------------------------------------------------------------
|
||||
|
||||
(defn ^:export run
|
||||
[]
|
||||
(dispatch-sync [:initialize]) ;; puts a value into application state
|
||||
(reagent/render [ui] ;; mount the application's ui into '<div id="app" />'
|
||||
(js/document.getElementById "app")))
|
||||
|
@ -1,196 +0,0 @@
|
||||
(ns simpleexample.core
|
||||
(:require-macros [reagent.ratom :refer [reaction]])
|
||||
(:require [reagent.core :as reagent]
|
||||
[re-frame.core :as rf]))
|
||||
|
||||
|
||||
;; Read this file from the top towards the bottom.
|
||||
|
||||
;; Normally I'd recomended you write a schema for the appplication
|
||||
;; state. But I'm going to skip that step to keep things simple.
|
||||
;;
|
||||
;; Suffice it to say the application state looks like this:
|
||||
;; {:time (js/Date.)
|
||||
;; :time-color "#f88"}
|
||||
;;
|
||||
;; See FAQ #1 for details on how to inspect application state.
|
||||
|
||||
;; -- Domino 1 - Events -------------------------------------------------------
|
||||
;;
|
||||
;; To send an event you call `dispatch` with a single vector argument.
|
||||
;;
|
||||
;; (dispatch [:event-id value1 value2])
|
||||
;;
|
||||
;; The first element in the vector identifies the kind of event. The
|
||||
;; further elements are optional, additional data associated with the event.
|
||||
;;
|
||||
;; Below we send a `:timer` event every second. But, typically,
|
||||
;; events are `dispatch`ed when a button is clicked or an
|
||||
;; HTTP POST's on-success is called or a web sockets gets a new packet.
|
||||
;;
|
||||
;; So `dispatch` gets called in various places.
|
||||
;;
|
||||
;; Later, when we get to Domino 5 (view functions) you'll see
|
||||
;; an example of DOM elements causing a `dispatch`.
|
||||
|
||||
(defn dispatch-timer-event
|
||||
[]
|
||||
(let [now (js/Date.)]
|
||||
(rf/dispatch [:timer now])))
|
||||
|
||||
;; an event will be dispatched every second
|
||||
(defonce time-updater (js/setInterval dispatch-timer-event 1000))
|
||||
|
||||
|
||||
;; -- Domino 2 - Event Handlers -----------------------------------------------
|
||||
;;
|
||||
;; Associate a "handler function" with each kind of event.
|
||||
;;
|
||||
;; This application has three events, identified by keywords:
|
||||
;; :initialise
|
||||
;; :time-color-change
|
||||
;; :timer
|
||||
;;
|
||||
;; Because there's 3 events, below you'll see `reg-event-db`
|
||||
;; called 3 times, registering 3 handlers, each like this:
|
||||
;; (reg-event-db
|
||||
;; :a-keyword-id-for-the-event
|
||||
;; a-function-which-will-handle-the-event)
|
||||
;;
|
||||
;; Any event handler registered via `reg-event-db` will look like this:
|
||||
;; (fn [db v]
|
||||
;; ...)
|
||||
;; It will take two parameters, the current application state `db`
|
||||
;; and the event vector `v` which is being handled.
|
||||
;; It must compute and return the new state of the application.
|
||||
;;
|
||||
;; Below, you'll sometimes see it written as:
|
||||
;; (fn [db [_ something]] <-- 2nd vec param destructured to get payload value
|
||||
;; ...)
|
||||
;;
|
||||
;; An event handler return `effects`. We're using
|
||||
;; `reg-event-db` here which assumes the only effect required (for this event)
|
||||
;; is a change to application state. When you need to do other effects,
|
||||
;; like sending emails, or http POSTing, or writing to localstore, you
|
||||
;; would use the more sophisticated `reg-event-fx` (note the trailing -fx) which
|
||||
;; is described in the tutorial. We're keeping it simple for the moment.
|
||||
|
||||
(rf/reg-event-db ;; sets up initial application state
|
||||
:initialize ;; usage: (dispatch [:initialize])
|
||||
(fn [_ _] ;; the two parameters are not important here, so use _
|
||||
{:time (js/Date.) ;; What it returns becomes the new application state
|
||||
:time-color "#f88"})) ;; so the application state will initially be a map with two keys
|
||||
|
||||
|
||||
(rf/reg-event-db ;; usage: (dispatch [:time-color-change 34562])
|
||||
:time-color-change ;; dispatched when the user enters a new colour into the UI
|
||||
(fn [db [_ new-color-value]] ;; -db event handlers given 2 parameters: current application state and event (a vector)
|
||||
(assoc db :time-color new-color-value))) ;; compute and return the new application state
|
||||
|
||||
|
||||
(rf/reg-event-db ;; usage: (dispatch [:timer a-js-Date])
|
||||
:timer ;; every second an event of this kind will be dispatched
|
||||
(fn [db [_ new-time]] ;; note how the 2nd parameter is desctructure to obtain the data value
|
||||
(assoc db :time new-time))) ;; compute and return the new application state
|
||||
|
||||
|
||||
;; -- Domino 3 - Efect Handlers -----------------------------------------------
|
||||
;;
|
||||
;; You almost never have to write effect handlers. You just use the standard
|
||||
;; re-frame ones or, sometimes, ones from a library.
|
||||
;;
|
||||
;; In this simple app, the only effect returned by event handlers is that
|
||||
;; of changing application state, which is a standard effect, and even THAT
|
||||
;; effect is implied (rather than explicit) because we are using the
|
||||
;; helpful registration function `reg-event-db`
|
||||
;;
|
||||
;; Just know that effects are happening but we don't need to do anything. We'll
|
||||
;; learn more about effect handlers later.
|
||||
|
||||
|
||||
;; -- Domino 4 - Subscription Handlers ----------------------------------------
|
||||
;;
|
||||
;; These query functions take application state and return a "materialised view"
|
||||
;; of that state. A computation over the current application state.
|
||||
;;
|
||||
;; Now, the examples below are trivial. They just extract part of the application
|
||||
;; state and return it. Virtually no computation.
|
||||
;;
|
||||
;; More interesting subscriptions and more explanation can be found in the
|
||||
;; todomvc example.
|
||||
;;
|
||||
;; `reg-sub` associates a `query identifier` with a function which computes
|
||||
;; that query, like this:
|
||||
;; (reg-sub
|
||||
;; :some-query-id ;; query identifier
|
||||
;; some-function) ;; the function which will compute the query
|
||||
;; If ever a view requests data like this:
|
||||
;; `(listen [:some-query-id])` ;; note use of `:some-query-id`
|
||||
;; then `some-function` will be used to perform the query over applciation state
|
||||
;; when a
|
||||
;;
|
||||
;; Each time application state changes, `some-function` will be
|
||||
;; called again to compute a new materialised view (a new computation)
|
||||
;; and that new value will be given to any view function which is subscribed
|
||||
;; to `:some-query-id`. The view function itself will then also be called again
|
||||
;; to create new DOM (because it depends on a query value which changed).
|
||||
;;
|
||||
;; re-frame will ensure the necessary calls are made, at the right time.
|
||||
;;
|
||||
(rf/reg-sub
|
||||
:time
|
||||
(fn [db _] ;; db is current app state. 2nd usused param is query vector
|
||||
(:time db))) ;; return a query computation over the application state
|
||||
|
||||
(rf/reg-sub
|
||||
:time-color
|
||||
(fn [db _]
|
||||
(:time-color db)))
|
||||
|
||||
|
||||
;; -- Domino 5 - View Functions ----------------------------------------------
|
||||
;;
|
||||
;; We're using Reagent here.
|
||||
;; These functions turn data into DOM. They render the UI.
|
||||
;; The data they return is a hiccup formated description of the DOM required.
|
||||
;; They take two sources of data:
|
||||
;; - arguments
|
||||
;; - queries which obtain data from the application state.
|
||||
;;
|
||||
;;
|
||||
;; Noitce that color-input has a dispatch
|
||||
|
||||
(defn clock
|
||||
[]
|
||||
[:div.example-clock
|
||||
{:style {:color (rf/listen [:time-color])}}
|
||||
(-> (rf/listen [:time])
|
||||
.toTimeString
|
||||
(clojure.string/split " ")
|
||||
first)])
|
||||
|
||||
(defn color-input
|
||||
[]
|
||||
[:div.color-input
|
||||
"Time color: "
|
||||
[:input {:type "text"
|
||||
:value (rf/listen [:time-color])
|
||||
:on-change #(rf/dispatch [:time-color-change (-> % .-target .-value)])}]]) ;; <---
|
||||
|
||||
(defn ui
|
||||
[]
|
||||
[:div
|
||||
[:h1 "Hello world, it is now"]
|
||||
[clock]
|
||||
[color-input]])
|
||||
|
||||
|
||||
;; -- Entry Point -------------------------------------------------------------
|
||||
|
||||
|
||||
(defn ^:export run
|
||||
[]
|
||||
(dispatch-sync [:initialize]) ;; puts a value into application state
|
||||
(reagent/render [ui] ;; mount the application's ui into '<div id="app" />'
|
||||
(js/document.getElementById "app")))
|
||||
|
BIN
images/Readme/Dominos.jpg
Normal file
BIN
images/Readme/Dominos.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
Loading…
x
Reference in New Issue
Block a user