Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
20c59a243f
102
README.md
102
README.md
|
@ -57,12 +57,11 @@ When you use re-frame, you'll create your app by writing three kinds of function
|
||||||
- components - which turn data into hiccup (DOM)
|
- components - which turn data into hiccup (DOM)
|
||||||
- event handlers - which provide the state transition (control) layer
|
- event handlers - which provide the state transition (control) layer
|
||||||
|
|
||||||
You'll also be designing a data structure to represent the app state, and probably writing a [herbet schema](https://github.com/miner/herbert) for it.
|
You'll also be designing a data structure to represent the app state, and probably writing a [herbert schema](https://github.com/miner/herbert) for it.
|
||||||
|
|
||||||
## The Parts
|
## The Parts
|
||||||
|
|
||||||
To teach re-frame, I'll now incrementally
|
To teach re-frame, I'll now incrementally develop a diagram, explaining each part as it is added.
|
||||||
develop a diagram, explaining each part as it is added.
|
|
||||||
|
|
||||||
Along the way, I'll be using [reagent] at an intermediate to advanced level. This is not an introduction to reagent tutorial, so you need to have done one of those before getting here. Try
|
Along the way, I'll be using [reagent] at an intermediate to advanced level. This is not an introduction to reagent tutorial, so you need to have done one of those before getting here. Try
|
||||||
[the official intro](http://reagent-project.github.io/) or
|
[the official intro](http://reagent-project.github.io/) or
|
||||||
|
@ -75,6 +74,7 @@ Along the way, I'll be using [reagent] at an intermediate to advanced level. Thi
|
||||||
##### The Big Ratom
|
##### The Big Ratom
|
||||||
|
|
||||||
Our re-frame diagram starts with the "well-formed data at rest" bit:
|
Our re-frame diagram starts with the "well-formed data at rest" bit:
|
||||||
|
|
||||||
```
|
```
|
||||||
app-db
|
app-db
|
||||||
```
|
```
|
||||||
|
@ -82,7 +82,7 @@ app-db
|
||||||
re-frame recommends that you put your data into one place which we'll call `app-db`. Structure the data in that place, of course. And [give it a schema](https://github.com/miner/herbert).
|
re-frame recommends that you put your data into one place which we'll call `app-db`. Structure the data in that place, of course. And [give it a schema](https://github.com/miner/herbert).
|
||||||
|
|
||||||
Now, this advice is not the slightest bit controversial for 'real' databases, right?
|
Now, this advice is not the slightest bit controversial for 'real' databases, right?
|
||||||
You'd happily put all your well-formed data into Postgres or mysql. But within a running application (in memory), it is different. If you have a background in OO, this data-in-one-place is a
|
You'd happily put all your well-formed data into PostgreSQL or MySQL. But within a running application (in memory), it is different. If you have a background in OO, this data-in-one-place is a
|
||||||
hard one to swallow. You've
|
hard one to swallow. You've
|
||||||
spent your life breaking systems into pieces, organised around behaviour and trying
|
spent your life breaking systems into pieces, organised around behaviour and trying
|
||||||
to hide the data. I still wake up in a sweat some nights thinking about all
|
to hide the data. I still wake up in a sweat some nights thinking about all
|
||||||
|
@ -91,12 +91,13 @@ that Clojure data lying around exposed and passive.
|
||||||
But, as @fogus tells us, data is the easy bit.
|
But, as @fogus tells us, data is the easy bit.
|
||||||
|
|
||||||
From here on, we'll assume `app-db` is one of these:
|
From here on, we'll assume `app-db` is one of these:
|
||||||
```
|
|
||||||
|
```Clojure
|
||||||
(def app-db (reagent/atom {})) ;; a reagent atom, containing a map
|
(def app-db (reagent/atom {})) ;; a reagent atom, containing a map
|
||||||
```
|
```
|
||||||
|
|
||||||
Although it is a reagent atom (ratom), I'd encourage you to think of it as an in-memory database.
|
Although it is a reagent atom (`ratom`), I'd encourage you to think of it as an in-memory database.
|
||||||
It will contain structured data (perhaps with a formal [Herbet Schema] spec).
|
It will contain structured data (perhaps with a formal [Herbert Schema] spec).
|
||||||
You will need to query that data. You will perform CRUD
|
You will need to query that data. You will perform CRUD
|
||||||
and other transformations on it. You'll often want to transact on this
|
and other transformations on it. You'll often want to transact on this
|
||||||
database atomically, etc. So "in-memory database"
|
database atomically, etc. So "in-memory database"
|
||||||
|
@ -119,18 +120,18 @@ I'm going to quote verbatim from Elm's website:
|
||||||
|
|
||||||
Reagent provides a `ratom` (reagent atom) and a `reaction`. These are **two key building blocks**.
|
Reagent provides a `ratom` (reagent atom) and a `reaction`. These are **two key building blocks**.
|
||||||
|
|
||||||
Mechanically, `ratoms` are like normal ClojureScript atoms. You can `swap!` and `reset!` them, `watch` them, etc. Mechanically, it holds mutable data. **Conceptually**, though we'll tweak that paradigm ever so slightly. **We view a ratom is a value that changes over time.** This means we'll view it as an FRP [Signal](http://elm-lang.org/learn/What-is-FRP.elm).
|
Mechanically, `ratoms` are like normal ClojureScript atoms. You can `swap!` and `reset!` them, `watch` them, etc. Mechanically, it holds mutable data. **Conceptually**, though we'll tweak that paradigm ever so slightly. **We view a `ratom` as being a value that changes over time.** This means we'll view it as an FRP [Signal](http://elm-lang.org/learn/What-is-FRP.elm).
|
||||||
|
|
||||||
`reaction` acts a bit like a function. It's a macro which wraps some `computation` (some block of code) and returns a `ratom` containing the result of that `computation`.
|
`reaction` acts a bit like a function. It's a macro which wraps some `computation` (some block of code) and returns a `ratom` containing the result of that `computation`.
|
||||||
|
|
||||||
The computation performed by a `reaction` may involve dereferencing one or more ratoms.
|
The computation performed by a `reaction` may involve dereferencing one or more `ratoms`.
|
||||||
|
|
||||||
A `reaction` will automatically rerun its `computation` whenever any of these dereferenced ratoms change.
|
A `reaction` will automatically rerun its `computation` whenever any of these dereferenced `ratoms` change.
|
||||||
So, the ratom returned by a `reaction` is itself a Signal. Its value will change over time as its input Signals (the dereferenced ratoms) change.
|
So, the `ratom` returned by a `reaction` is itself a Signal. Its value will change over time as its input Signals (the dereferenced `ratoms`) change.
|
||||||
|
|
||||||
So values can 'flow' into computations and out again, and then into other computations, etc. The result is some sort of signal graph. But our graph will be without cycles, because cycles are bad!
|
So values can 'flow' into computations and out again, and then into other computations, etc. The result is some sort of signal graph. But our graph will be without cycles, because cycles are bad!
|
||||||
|
|
||||||
While the mechanics are different, `reaction` has the intent of `lift' in [Elm] and `defc=` in [hoplon].
|
While the mechanics are different, `reaction` has the intent of `lift` in [Elm] and `defc=` in [Hoplon].
|
||||||
|
|
||||||
Some code to clarify:
|
Some code to clarify:
|
||||||
|
|
||||||
|
@ -150,7 +151,7 @@ Some code to clarify:
|
||||||
;; - app-db in one case
|
;; - app-db in one case
|
||||||
;; - ratom1 in the other
|
;; - ratom1 in the other
|
||||||
;; Notice that both reactions above return a ratom.
|
;; Notice that both reactions above return a ratom.
|
||||||
;; Those returned ratoms hold the (time varing) value of the computations.
|
;; Those returned ratoms hold the (time varying) value of the computations.
|
||||||
|
|
||||||
(println @ratom2) ;; ==> {:b 1} ;; a computed result, involving @app-db
|
(println @ratom2) ;; ==> {:b 1} ;; a computed result, involving @app-db
|
||||||
(println @ratom3) ;; ==> "Hello" ;; a computed result, involving @ratom2
|
(println @ratom3) ;; ==> "Hello" ;; a computed result, involving @ratom2
|
||||||
|
@ -163,27 +164,32 @@ Some code to clarify:
|
||||||
(println @ratom3) ;; ==> "World" ;; ratom3 is automatically updated too.
|
(println @ratom3) ;; ==> "World" ;; ratom3 is automatically updated too.
|
||||||
```
|
```
|
||||||
|
|
||||||
So, in FRP terms, a `reaction` will produce a "stream" of values (it is a Signal), accessible via the ratom it returns.
|
So, in FRP terms, a `reaction` will produce a "stream" of values (it is a Signal), accessible via the `ratom` it returns.
|
||||||
|
|
||||||
Okay, so that was all important background information for what is to follow. Back to the diagram...
|
Okay, so that was all important background information for what is to follow. Back to the diagram...
|
||||||
|
|
||||||
### The Components
|
### The Components
|
||||||
|
|
||||||
Extending the diagram a bit, we introduce `components`:
|
Extending the diagram a bit, we introduce `components`:
|
||||||
|
|
||||||
```
|
```
|
||||||
app-db --> components --> hiccup
|
app-db --> components --> hiccup
|
||||||
```
|
```
|
||||||
|
|
||||||
When using reagent, your primary job is to write one or more `components`.
|
When using reagent, your primary job is to write one or more `components`.
|
||||||
|
|
||||||
Think about `components` as `pure functions` - data in, hiccup out. `hiccup` is
|
Think about `components` as `pure functions` - data in, hiccup out. `hiccup` is
|
||||||
ClojureScript data structures which represent DOM. Here's a trivial component:
|
ClojureScript data structures which represent DOM. Here's a trivial component:
|
||||||
```
|
|
||||||
|
```Clojure
|
||||||
(defn greet
|
(defn greet
|
||||||
[]
|
[]
|
||||||
[:div "Hello ratoms and reactions"])
|
[:div "Hello ratoms and reactions"])
|
||||||
```
|
```
|
||||||
|
|
||||||
And if we call it:
|
And if we call it:
|
||||||
```
|
|
||||||
|
```Clojure
|
||||||
(greet)
|
(greet)
|
||||||
;; ==> [:div "Hello ratoms and reactions"]
|
;; ==> [:div "Hello ratoms and reactions"]
|
||||||
```
|
```
|
||||||
|
@ -191,7 +197,8 @@ And if we call it:
|
||||||
You'll notice that our component is a regular Clojure function, nothing special. In this case, it takes no parameters and it returns a ClojureScript vector (hiccup).
|
You'll notice that our component is a regular Clojure function, nothing special. In this case, it takes no parameters and it returns a ClojureScript vector (hiccup).
|
||||||
|
|
||||||
Here is a slightly more interesting (parameterised) component (function):
|
Here is a slightly more interesting (parameterised) component (function):
|
||||||
```
|
|
||||||
|
```Clojure
|
||||||
(defn greet ;; greet now has a parameter
|
(defn greet ;; greet now has a parameter
|
||||||
[name] ;; 'name' is a ratom containing a string
|
[name] ;; 'name' is a ratom containing a string
|
||||||
[:div "Hello " @name]) ;; dereference 'name' here to extract the value it contains
|
[:div "Hello " @name]) ;; dereference 'name' here to extract the value it contains
|
||||||
|
@ -209,7 +216,8 @@ So components are easy - they are functions which turn data into hiccup (which w
|
||||||
Now, we're now going to introduce `reaction` into this mix. On the one hand, I'm complicating things by doing this, because reagent allows you to be ignorant of the mechanics I'm about to show you. It invisibly wraps your components in a `reaction` allowing you to be blissfully ignorant of how the magic happens.
|
Now, we're now going to introduce `reaction` into this mix. On the one hand, I'm complicating things by doing this, because reagent allows you to be ignorant of the mechanics I'm about to show you. It invisibly wraps your components in a `reaction` allowing you to be blissfully ignorant of how the magic happens.
|
||||||
|
|
||||||
On the other hand, it is useful to understand exactly how the Signal graph is wired. AND, in a minute, when we get to subscriptions, we ourselves will be actively using `reaction`, so we might as well bite the bullet here and now ... and, anyway, it is easy...
|
On the other hand, it is useful to understand exactly how the Signal graph is wired. AND, in a minute, when we get to subscriptions, we ourselves will be actively using `reaction`, so we might as well bite the bullet here and now ... and, anyway, it is easy...
|
||||||
```
|
|
||||||
|
```Clojure
|
||||||
(defn greet
|
(defn greet
|
||||||
[name] ;; name is a ratom
|
[name] ;; name is a ratom
|
||||||
[:div "Hello " @name]) ;; dereference name here, to extract the value within
|
[:div "Hello " @name]) ;; dereference name here, to extract the value within
|
||||||
|
@ -243,7 +251,7 @@ This is one way data flow, with FRP-nature.
|
||||||
|
|
||||||
I haven't been entirely straight with you:
|
I haven't been entirely straight with you:
|
||||||
1. reagent re-runs `reactions` (re-computations) via requestAnimationFrame. So a recomputation happens about 16ms after the need for it is detected, or after the current thread of processing finishes, whichever is the greater. So if you are in a bREPL and you run the lines of code above one after the other too quickly, you might not see the re-computation done immediately after `n` gets reset!, because the animationFrame hasn't run (yet). You could add a `(reagent.core/flush)` after the reset! to force re-computation to happen straight away.
|
1. reagent re-runs `reactions` (re-computations) via requestAnimationFrame. So a recomputation happens about 16ms after the need for it is detected, or after the current thread of processing finishes, whichever is the greater. So if you are in a bREPL and you run the lines of code above one after the other too quickly, you might not see the re-computation done immediately after `n` gets reset!, because the animationFrame hasn't run (yet). You could add a `(reagent.core/flush)` after the reset! to force re-computation to happen straight away.
|
||||||
2. `reaction` doesn't actually return a `ratom`. But it returns something that has ratom-nature, so we'll happily continue believing it is a ratom and no harm will come to us.
|
2. `reaction` doesn't actually return a `ratom`. But it returns something that has ratom-nature, so we'll happily continue believing it is a `ratom` and no harm will come to us.
|
||||||
|
|
||||||
On with the rest of my lies and distortions...
|
On with the rest of my lies and distortions...
|
||||||
|
|
||||||
|
@ -277,7 +285,8 @@ app-db --> components --> hiccup --> Reagent --> VDOM --> React --> DOM
|
||||||
```
|
```
|
||||||
|
|
||||||
In abstract, Clojure syntax terms, you could squint and imagine the process as:
|
In abstract, Clojure syntax terms, you could squint and imagine the process as:
|
||||||
```
|
|
||||||
|
```Clojure
|
||||||
(-> app-db
|
(-> app-db
|
||||||
components ;; produces Hiccup
|
components ;; produces Hiccup
|
||||||
reagent ;; produces VDOM (virtual DOM)
|
reagent ;; produces VDOM (virtual DOM)
|
||||||
|
@ -291,6 +300,7 @@ But, just to be clear, we don't have to bother ourselves with most of the pipel
|
||||||
### Subscribe
|
### Subscribe
|
||||||
|
|
||||||
In the beginning was the word, and the word was data. Then, all of a sudden, components happened...
|
In the beginning was the word, and the word was data. Then, all of a sudden, components happened...
|
||||||
|
|
||||||
```
|
```
|
||||||
app-db --> components
|
app-db --> components
|
||||||
```
|
```
|
||||||
|
@ -303,14 +313,15 @@ So let's pause to consider **our dream solution** for this part of the flow. `co
|
||||||
|
|
||||||
re-frame's `subscriptions` are an attempt to live this dream. As you'll see, they fall short on a couple of points, but they're not too bad.
|
re-frame's `subscriptions` are an attempt to live this dream. As you'll see, they fall short on a couple of points, but they're not too bad.
|
||||||
|
|
||||||
As the app developer, your job is to write and register one or more "subscription handlers" (functions that do a query). Your subscription functions must return a value that changes over time (Signal). i.e. they'll be returning a reaction (ratom).
|
As the app developer, your job is to write and register one or more "subscription handlers" (functions that do a query). Your subscription functions must return a value that changes over time (Signal). i.e. they'll be returning a reaction (`ratom`).
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
- `components` never source data directly from `app-db`, and instead, they use a subscription.
|
- `components` never source data directly from `app-db`, and instead, they use a subscription.
|
||||||
- subscriptions are only ever used by components (they are never used in event handlers).
|
- subscriptions are only ever used by components (they are never used in event handlers).
|
||||||
|
|
||||||
Here's a component using a subscription:
|
Here's a component using a subscription:
|
||||||
```
|
|
||||||
|
```Clojure
|
||||||
(defn greet ;; outer, setup function, called once
|
(defn greet ;; outer, setup function, called once
|
||||||
[]
|
[]
|
||||||
(let [name-ratom (subscribe [:name-query])] ;; <---- subscribe here
|
(let [name-ratom (subscribe [:name-query])] ;; <---- subscribe here
|
||||||
|
@ -323,21 +334,26 @@ First, note this is a form-2 `component` (there are 3 forms). Previously above,
|
||||||
- the outer function is a setup function, called once to initialise the component. Notice the use of 'subscribe' with the parameter `:name-query`. That creates a Signal through which new values are supplied over time.
|
- the outer function is a setup function, called once to initialise the component. Notice the use of 'subscribe' with the parameter `:name-query`. That creates a Signal through which new values are supplied over time.
|
||||||
|
|
||||||
`subscribe` is called like this:
|
`subscribe` is called like this:
|
||||||
```
|
|
||||||
|
```Clojure
|
||||||
(subscribe [query-id some optional query parameters])
|
(subscribe [query-id some optional query parameters])
|
||||||
```
|
```
|
||||||
|
|
||||||
There is only one subscribe function. We must register our `handlers` with it.
|
There is only one subscribe function. We must register our `handlers` with it.
|
||||||
|
|
||||||
The first element in the vector (`query-id`) identifies the query and the other elements are optional, query parameters. With a traditional database a query might be:
|
The first element in the vector (`query-id`) identifies the query and the other elements are optional, query parameters. With a traditional database a query might be:
|
||||||
|
|
||||||
```
|
```
|
||||||
select from customers where name="blah"
|
select from customers where name="blah"
|
||||||
```
|
```
|
||||||
|
|
||||||
In re-frame land, that would be done as follows:
|
In re-frame land, that would be done as follows:
|
||||||
(subscribe [:customer-query "blah"])
|
(subscribe [:customer-query "blah"])
|
||||||
which would return a ratom holding the customer state (might change over time!).
|
which would return a `ratom` holding the customer state (might change over time!).
|
||||||
|
|
||||||
Of course, for this to work, we must write and register a handler for `:customer-query`
|
Of course, for this to work, we must write and register a handler for `:customer-query`
|
||||||
```
|
|
||||||
|
```Clojure
|
||||||
(defn customer-query ;; a query over 'app-db' which returns a customer
|
(defn customer-query ;; a query over 'app-db' which returns a customer
|
||||||
[db, [sid cid]] ;; query fns are given 'app-db', plus vector given to subscribe
|
[db, [sid cid]] ;; query fns are given 'app-db', plus vector given to subscribe
|
||||||
(assert (= sid :customer-query)) ;; subscription id was the first vector
|
(assert (= sid :customer-query)) ;; subscription id was the first vector
|
||||||
|
@ -352,7 +368,8 @@ Of course, for this to work, we must write and register a handler for `:customer
|
||||||
**Note**: `components` tend to be organised into a hierarchy, often with data flowing from parent to child via parameters. So not every component needs a subscription.
|
**Note**: `components` tend to be organised into a hierarchy, often with data flowing from parent to child via parameters. So not every component needs a subscription.
|
||||||
|
|
||||||
**Rule**: subscriptions can only be used in form-2 components and the subscription must be in the outer setup function and not in the inner render function. So the following is **wrong** (compare to the correct version above)
|
**Rule**: subscriptions can only be used in form-2 components and the subscription must be in the outer setup function and not in the inner render function. So the following is **wrong** (compare to the correct version above)
|
||||||
```
|
|
||||||
|
```Clojure
|
||||||
(defn greet ;; a form-1 component - no inner render function
|
(defn greet ;; a form-1 component - no inner render function
|
||||||
[]
|
[]
|
||||||
(let [name-ratom (subscribe [:name-query])] ;; Eek! subscription in render part
|
(let [name-ratom (subscribe [:name-query])] ;; Eek! subscription in render part
|
||||||
|
@ -365,7 +382,7 @@ Getting more complicated ...
|
||||||
|
|
||||||
Imagine our `app-db` contains some `items` (a vector of maps). And imagine that we must display these items sorted by one of their attributes attribute. We could write this query-handler:
|
Imagine our `app-db` contains some `items` (a vector of maps). And imagine that we must display these items sorted by one of their attributes attribute. We could write this query-handler:
|
||||||
|
|
||||||
```
|
```Clojure
|
||||||
(register
|
(register
|
||||||
:sorted-items ;; the query id
|
:sorted-items ;; the query id
|
||||||
(fn [db [_ sort-kw] ;; sort-kw is a ratom, contains a keyword.
|
(fn [db [_ sort-kw] ;; sort-kw is a ratom, contains a keyword.
|
||||||
|
@ -374,11 +391,13 @@ Imagine our `app-db` contains some `items` (a vector of maps). And imagine that
|
||||||
(let [items (get-in @db [:some :path :items])] ;; get the items
|
(let [items (get-in @db [:some :path :items])] ;; get the items
|
||||||
(sort-by @sort-kw items))))) ;; return them sorted
|
(sort-by @sort-kw items))))) ;; return them sorted
|
||||||
```
|
```
|
||||||
|
|
||||||
First, notice that this reaction involves 2 input Signals: db and sort-kw.
|
First, notice that this reaction involves 2 input Signals: db and sort-kw.
|
||||||
If either changes, the query is re-run. That means it will be re-run if the items change OR the sort attribute changes.
|
If either changes, the query is re-run. That means it will be re-run if the items change OR the sort attribute changes.
|
||||||
|
|
||||||
We'd use it like this:
|
We'd use it like this:
|
||||||
```
|
|
||||||
|
```Clojure
|
||||||
(defn items-list ;; outer, setup function, called once
|
(defn items-list ;; outer, setup function, called once
|
||||||
[]
|
[]
|
||||||
(let [by-this (reagent/atom :name) ;; sort by :name attribute, GUI might reset! somehow
|
(let [by-this (reagent/atom :name) ;; sort by :name attribute, GUI might reset! somehow
|
||||||
|
@ -392,10 +411,11 @@ We'd use it like this:
|
||||||
|
|
||||||
There's a bit going on in that `let`, most of it highly contrived, just so I can show off chained reactions. Okay, okay. All I wanted was an excuse to use the phrase chained reactions.
|
There's a bit going on in that `let`, most of it highly contrived, just so I can show off chained reactions. Okay, okay. All I wanted was an excuse to use the phrase chained reactions.
|
||||||
|
|
||||||
In reality, the approach taken above is inefficient. Every time `app-db` changes, the `:sorted-items` query is going to be re-run and it's going to re-sort items. But items might not have changed since last time. Some other part of app-db may have changed. We don't want to re-sort items each time something unrelated changes.
|
In reality, the approach taken above is inefficient. Every time `app-db` changes, the `:sorted-items` query is going to be re-run and it's going to re-sort items. But items might not have changed since last time. Some other part of `app-db` may have changed. We don't want to re-sort items each time something unrelated changes.
|
||||||
|
|
||||||
We can fix that up:
|
We can fix that up:
|
||||||
```
|
|
||||||
|
```Clojure
|
||||||
(register
|
(register
|
||||||
:sorted-items ;; the query id
|
:sorted-items ;; the query id
|
||||||
(fn [db [_ sort-kw] ;; sort-kw is a ratom containing the attribute to sort on
|
(fn [db [_ sort-kw] ;; sort-kw is a ratom containing the attribute to sort on
|
||||||
|
@ -404,7 +424,7 @@ We can fix that up:
|
||||||
(reaction (sort-by @sort-kw @items))))) ;; reaction #2
|
(reaction (sort-by @sort-kw @items))))) ;; reaction #2
|
||||||
```
|
```
|
||||||
|
|
||||||
Be aware that the second reaction will only be triggered if `items` does not test `identical?` to the previous value. **Yes, that sort of optimisation is built into chain `reactions`.** Which means the component render function (which is wrapped in another reaction) won't rerun if app-db changes, unless items changes. Now we're very efficient.
|
Be aware that the second reaction will only be triggered if `items` does not test `identical?` to the previous value. **Yes, that sort of optimisation is built into chain `reactions`.** Which means the component render function (which is wrapped in another reaction) won't rerun if `app-db` changes, unless items changes. Now we're very efficient.
|
||||||
|
|
||||||
If I were doing this for real (rather than just demoing possibilities), I'd probably create a simple subscription for items (unsorted), and then do the sort in the component itself (as a reaction, similar to how 'num' is done in the example above). After all, it is the component which needs to show sorted. It can contain the sorting, which might involve the
|
If I were doing this for real (rather than just demoing possibilities), I'd probably create a simple subscription for items (unsorted), and then do the sort in the component itself (as a reaction, similar to how 'num' is done in the example above). After all, it is the component which needs to show sorted. It can contain the sorting, which might involve the
|
||||||
|
|
||||||
|
@ -441,7 +461,7 @@ In re-frame, the backward data flow of events happens via a conveyor belt:
|
||||||
app-db --> components --> Hiccup --> Reagent --> VDOM --> React --> DOM
|
app-db --> components --> Hiccup --> Reagent --> VDOM --> React --> DOM
|
||||||
^ |
|
^ |
|
||||||
| v
|
| v
|
||||||
handlers <------------------- events ---------------------------------------
|
handlers <------------------- events -------------------------------------------
|
||||||
a "conveyor belt" takes events
|
a "conveyor belt" takes events
|
||||||
from the DOM to the handlers
|
from the DOM to the handlers
|
||||||
```
|
```
|
||||||
|
@ -466,10 +486,12 @@ The first item in the vector identifies the event and
|
||||||
the rest of the vector is the optional parameters -- in this case, the id (42) of the item to delete.
|
the rest of the vector is the optional parameters -- in this case, the id (42) of the item to delete.
|
||||||
|
|
||||||
Here are some other example events:
|
Here are some other example events:
|
||||||
|
|
||||||
```Clojure
|
```Clojure
|
||||||
[:set-spam-wanted false]
|
[:set-spam-wanted false]
|
||||||
[[:complicated :multi :part :key] "a parameter" "another one" 45.6]
|
[[:complicated :multi :part :key] "a parameter" "another one" 45.6]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Rule**: events are pure data. No dirty tricks like putting callbacks on the wire. You know who you are.
|
**Rule**: events are pure data. No dirty tricks like putting callbacks on the wire. You know who you are.
|
||||||
|
|
||||||
### Dispatching Events
|
### Dispatching Events
|
||||||
|
@ -477,6 +499,7 @@ Here are some other example events:
|
||||||
Events start in the DOM. They are `dispatched`.
|
Events start in the DOM. They are `dispatched`.
|
||||||
|
|
||||||
For example, a button component might be like this:
|
For example, a button component might be like this:
|
||||||
|
|
||||||
```Clojure
|
```Clojure
|
||||||
(defn yes-button
|
(defn yes-button
|
||||||
[]
|
[]
|
||||||
|
@ -486,6 +509,7 @@ For example, a button component might be like this:
|
||||||
```
|
```
|
||||||
|
|
||||||
Notice the `on-click` handler:
|
Notice the `on-click` handler:
|
||||||
|
|
||||||
```Clojure
|
```Clojure
|
||||||
#(dispatch [:yes-button-clicked])
|
#(dispatch [:yes-button-clicked])
|
||||||
```
|
```
|
||||||
|
@ -495,11 +519,12 @@ With re-frame, we try to keep the DOM as passive as possible. It is simply a re
|
||||||
There is a single `dispatch` function in the entire app, and it takes only one parameter, the event vector.
|
There is a single `dispatch` function in the entire app, and it takes only one parameter, the event vector.
|
||||||
|
|
||||||
Let's update our diagram to show dispatch:
|
Let's update our diagram to show dispatch:
|
||||||
|
|
||||||
```
|
```
|
||||||
app-db --> components --> Hiccup --> Reagent --> VDOM --> React --> DOM
|
app-db --> components --> Hiccup --> Reagent --> VDOM --> React --> DOM
|
||||||
^ |
|
^ |
|
||||||
| v
|
| v
|
||||||
handlers <------------------------------------- (dispatch [event-id other params])
|
handlers <---------------------------------------- (dispatch [event-id other params])
|
||||||
```
|
```
|
||||||
|
|
||||||
**Rule**: `components` are as passive as possible when it comes to handling events. Do the minimum. On the other hand, `components` can be as complex as needed when it comes to creating the visuals.
|
**Rule**: `components` are as passive as possible when it comes to handling events. Do the minimum. On the other hand, `components` can be as complex as needed when it comes to creating the visuals.
|
||||||
|
@ -511,12 +536,13 @@ Collectively, event handlers provide the control logic in the applications.
|
||||||
Almost all event handlers mutate `app-db` in some way. Adding an item here, or deleting that one there. So often CRUD, but sometimes much more. Sometimes with async results.
|
Almost all event handlers mutate `app-db` in some way. Adding an item here, or deleting that one there. So often CRUD, but sometimes much more. Sometimes with async results.
|
||||||
|
|
||||||
Even though handlers appear to be about `app-db` mutation, re-frame requires them to be pure functions with a signature of:
|
Even though handlers appear to be about `app-db` mutation, re-frame requires them to be pure functions with a signature of:
|
||||||
|
|
||||||
```
|
```
|
||||||
(state-of-app-db, event-vector) -> new-state
|
(state-of-app-db, event-vector) -> new-state
|
||||||
```
|
```
|
||||||
re-frame passes to an event handler two parameters: the current state of `app-db` plus the event, and the job of a handler to return a modified version of the state (which re-frame will then put back into the `app-db`). XXX currently not true but it will be shortly.
|
re-frame passes to an event handler two parameters: the current state of `app-db` plus the event, and the job of a handler to return a modified version of the state (which re-frame will then put back into the `app-db`). XXX currently not true but it will be shortly.
|
||||||
|
|
||||||
```
|
```Clojure
|
||||||
(defn handle-delete
|
(defn handle-delete
|
||||||
[state [_ item-id]] ;; notice how event vector is destructured -- 2nd parameter
|
[state [_ item-id]] ;; notice how event vector is destructured -- 2nd parameter
|
||||||
(dissoc-in state [:some :path item-id])) ;; return a modified version of 'state'
|
(dissoc-in state [:some :path item-id])) ;; return a modified version of 'state'
|
||||||
|
@ -528,7 +554,7 @@ Because handlers are pure functions, and because they generally only have to han
|
||||||
|
|
||||||
`dispatch` has to call the right handler. Handlers have to be registered.
|
`dispatch` has to call the right handler. Handlers have to be registered.
|
||||||
|
|
||||||
```
|
```Clojure
|
||||||
(register
|
(register
|
||||||
:delete-item
|
:delete-item
|
||||||
handle-delete)
|
handle-delete)
|
||||||
|
@ -540,7 +566,7 @@ Above, I commented that collectively handler represent the control layer of the
|
||||||
|
|
||||||
A big part of what they do is to manage state transitions. The application is in state X, and event Y arrives, so the handler for Y was to move the app to state Z.
|
A big part of what they do is to manage state transitions. The application is in state X, and event Y arrives, so the handler for Y was to move the app to state Z.
|
||||||
|
|
||||||
Although I've done nothing to try and implement it, this is obviously fertile territory for using [statechars](http://www.amazon.com/Constructing-User-Interface-Statecharts-Horrocks/dp/0201342782).
|
Although I've done nothing to try and implement it, this is obviously fertile territory for using [statecharts](http://www.amazon.com/Constructing-User-Interface-Statecharts-Horrocks/dp/0201342782).
|
||||||
|
|
||||||
### Talking To The Server
|
### Talking To The Server
|
||||||
|
|
||||||
|
@ -564,7 +590,7 @@ To build an app using re-frame, you'll have to:
|
||||||
|
|
||||||
[SPAs]:http://en.wikipedia.org/wiki/Single-page_application
|
[SPAs]:http://en.wikipedia.org/wiki/Single-page_application
|
||||||
[reagent]:http://reagent-project.github.io/
|
[reagent]:http://reagent-project.github.io/
|
||||||
[Dan Holmsand]:https://github.com/holmsand
|
[Dan Holmsand]:https://twitter.com/holmsand
|
||||||
[Hiccup]:https://github.com/weavejester/hiccup
|
[Hiccup]:https://github.com/weavejester/hiccup
|
||||||
[FRP]:https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
|
[FRP]:https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
|
||||||
[Elm]:http://elm-lang.org/
|
[Elm]:http://elm-lang.org/
|
||||||
|
@ -573,4 +599,4 @@ To build an app using re-frame, you'll have to:
|
||||||
[datascript]:https://github.com/tonsky/datascript
|
[datascript]:https://github.com/tonsky/datascript
|
||||||
[Hoplon]:http://hoplon.io/
|
[Hoplon]:http://hoplon.io/
|
||||||
[Pedestal App]:https://github.com/pedestal/pedestal-app
|
[Pedestal App]:https://github.com/pedestal/pedestal-app
|
||||||
[Herbet Schema]:https://github.com/miner/herbert
|
[Herbert Schema]:https://github.com/miner/herbert
|
||||||
|
|
Loading…
Reference in New Issue