20 |
21 |
--------------------------------------------------------------------------------
/CITATION.md:
--------------------------------------------------------------------------------
1 | To cite re-frame in publications, please use:
2 |
3 | [](https://doi.org/10.5281/zenodo.801613)
4 |
5 | Thompson, M. (2015, March). Re-Frame: A Reagent Framework For Writing SPAs, in Clojurescript.
6 | Zenodo. http://doi.org/10.5281/zenodo.801613
7 |
8 | @misc{thompson_2015,
9 | author = {Thompson, Michael},
10 | title = {Re-Frame: A Reagent Framework For Writing SPAs, in Clojurescript.},
11 | month = mar,
12 | year = 2015,
13 | doi = {10.5281/zenodo.801613},
14 | url = {https://doi.org/10.5281/zenodo.801613}
15 | }
16 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to re-frame
2 |
3 | Thank you for taking the time to contribute!
4 |
5 | ## Support questions
6 |
7 | The Github issues are for bug reports and feature requests only. Support requests and usage
8 | questions should go to the re-frame [Clojure Slack channel](http://clojurians.net) or
9 | the [ClojureScript mailing list](https://groups.google.com/forum/#!forum/clojurescript).
10 |
11 | ## Pull requests
12 |
13 | **Create pull requests to the master branch.**
14 |
15 | ## Running tests
16 |
17 | #### Via Browser/HTML
18 |
19 | To build the tests and run them in one step, just:
20 | ```sh
21 | lein test-once # compiles & then opens test.html in the browser
22 | ```
23 |
24 | You can also get auto compiles via:
25 | ```sh
26 | lein test-auto
27 | ```
28 | but you'll need to manually open `test/test.html` in a browser. And you'll also need to
29 | manually reload this page after each auto compile.
30 |
31 | #### Via Karma
32 |
33 | To run the tests, you must have recent versions of node, npm, Leiningen, and a C++ compiler
34 | toolchain installed. If you're on Linux or Mac OS X then you will be fine,
35 | if you're on Windows then you need to install Visual Studio Community Edition,
36 | and the C++ compiler dependencies.
37 |
38 | ```sh
39 | npm install karma-cli -g # Install the Karma CLI runner
40 | lein deps # runs lein-npm, installs Karma & other node dependencies. Only needed the first time.
41 | lein karma-once # to build re-frame tests
42 | karma start # to run the tests with an auto watcher
43 | ```
44 |
45 | ## Pull requests for bugs
46 |
47 | If possible provide:
48 |
49 | * Code that fixes the bug
50 | * Failing tests which pass with the new changes
51 | * Improvements to documentation to make it less likely that others will run into issues (if relevant).
52 | * Add the change to the Unreleased section of [CHANGES.md](CHANGES.md)
53 |
54 | ## Pull requests for features
55 |
56 | If possible provide:
57 |
58 | * Code that implements the new feature
59 | * Tests to cover the new feature including all of the code paths
60 | * Docstrings for functions
61 | * Documentation examples
62 | * Add the change to the Unreleased section of [CHANGES.md](CHANGES.md)
63 |
64 | ## Pull requests for docs
65 |
66 | * Make your documentation changes
67 | * (Optional) Install doctoc with `npm install -g doctoc`
68 | * (Optional) Regenerate the docs TOC with `bin/doctoc.sh` or `bin/doctoc.bat` depending on your OS
69 |
--------------------------------------------------------------------------------
/SUPPORT.md:
--------------------------------------------------------------------------------
1 | ## Support questions
2 |
3 | The Github issues are for bug reports and feature requests only. Support requests and usage
4 | questions should go to the re-frame [Clojure Slack channel](http://clojurians.net) or
5 | the [ClojureScript mailing list](https://groups.google.com/forum/#!forum/clojurescript).
6 |
--------------------------------------------------------------------------------
/bin/doctoc.bat:
--------------------------------------------------------------------------------
1 | :: Table of contents are generated by doctoc.
2 | :: Install doctoc with `npm install -g doctoc`
3 | :: Then run this script to regenerate the TOC after
4 | :: editing the docs.
5 |
6 | doctoc ./docs/ --github --title '## Table Of Contents'
7 |
--------------------------------------------------------------------------------
/bin/doctoc.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Table of contents are generated by doctoc.
3 | # Install doctoc with `npm install -g doctoc`
4 | # Then run this script to regenerate the TOC after
5 | # editing the docs.
6 |
7 | doctoc $(dirname $0)/../docs --github --title '## Table Of Contents'
8 |
--------------------------------------------------------------------------------
/book.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "re-frame",
3 | "description": "Documentation of re-frame framework",
4 | "language": "en",
5 | "root": "./docs/",
6 | "styles": {
7 | "website": "styles/website.css"
8 | },
9 | "structure": {
10 | "readme": "INTRO.md",
11 | "summary": "README.md"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 |
2 | ;; Allows users of deps.edn to more conveniently fork / make PRs to re-frame
3 |
4 | {:deps {net.cgrand/macrovich {:mvn/version "0.2.1"}
5 | org.clojure/tools.logging {:mvn/version "0.4.1"}
6 | com.lambdaisland/glogi {:mvn/version "1.0.116"}
7 | reagent/reagent {:mvn/version "0.9.1"
8 | :exclusions [cljsjs/react cljsjs/react-dom cljsjs/react-dom-server]}}
9 | :aliases
10 | {:dev
11 | {:extra-deps {org.clojure/clojure {:mvn/version "1.10.1"}
12 | org.clojure/clojurescript {:mvn/version "1.10.844"}
13 | }}}}
14 |
--------------------------------------------------------------------------------
/docs/API.md:
--------------------------------------------------------------------------------
1 | ## The re-frame API
2 |
3 | Orientation:
4 | 1. The API is provided by `re-frame.core`:
5 | - at some point, it would be worth your time [to browse it](/src/re_frame/core.cljc)
6 | - to use re-frame, you'll need to `require` it, perhaps like this ...
7 | ```clj
8 | (ns my.namespace
9 | (:require [re-frame.core :as rf]))
10 |
11 | ... now use rf/reg-event-fx or rf/subscribe
12 | ```
13 | 2. The API is small. Writing an app, you use less than 10 API functions. Maybe just 5.
14 | 3. There's no auto-generated docs [because of this problem](/src/re_frame/core.cljc#L23-L36)
15 | but, as a substitute,
16 | the links below take you to the doc-strings of often-used API functions.
17 |
18 | ## Doc Strings For The Significant API Functions
19 |
20 | The core API consists of:
21 | - [dispatch](/src/re_frame/router.cljc#L233-L243) or [dispatch-sync](/src/re_frame/router.cljc#L251-L263).
22 | - [reg-event-db](/src/re_frame/core.cljc#L71-L80) or [reg-event-fx](/src/re_frame/core.cljc#L87-L97)
23 | - [reg-sub](/src/re_frame/subs.cljc#L200-L329) and [subscribe](/src/re_frame/subs.cljc#L74-L115) working together
24 |
25 | Occasionally, you'll need to use:
26 | - [reg-fx](/src/re_frame/fx.cljc#L18-L41)
27 | - [reg-cofx](/src/re_frame/cofx.cljc#L14-L22) and [inject-cofx](/src/re_frame/cofx.cljc#L29-L80) working together
28 |
29 | And, finally, there are the builtin Interceptors:
30 | - [path](/src/re_frame/std_interceptors.cljc#L182-L208)
31 | - [after](/src/re_frame/std_interceptors.cljc#L295-L305)
32 | - [debug](/src/re_frame/std_interceptors.cljc#L14-L40)
33 | - and browse [the others](/src/re_frame/std_interceptors.cljc)
34 |
35 |
36 | ## Built-in Effect Handlers
37 |
38 | The following built-in effects are also a part of the API:
39 |
40 | #### :dispatch-later
41 |
42 | `dispatch` one or more events after given delays. Expects a collection
43 | of maps with two keys: `:ms` and `:dispatch`
44 |
45 | usage:
46 | ```clj
47 | {:dispatch-later [{:ms 200 :dispatch [:event-id "param"]}
48 | {:ms 100 :dispatch [:also :this :in :100ms]}]}
49 | ```
50 |
51 | Which means: in 200ms do this: `(dispatch [:event-id "param"])` and in 100ms ...
52 |
53 | Note: nil entries in the collection are ignored which means events can be added
54 | conditionally:
55 |
56 | ```clj
57 | {:dispatch-later [ (when (> 3 5) {:ms 200 :dispatch [:conditioned-out]})
58 | {:ms 100 :dispatch [:another-one]}]}
59 | ```
60 |
61 | ***
62 |
63 | #### :dispatch
64 |
65 | `dispatch` one event. Expects a single vector.
66 |
67 | usage:
68 | ```clj
69 | {:dispatch [:event-id "param"] }
70 | ```
71 |
72 | ***
73 |
74 | #### :dispatch-n
75 |
76 | `dispatch` more than one event. Expects a collection events.
77 |
78 | usage:
79 | ```clj
80 | {:dispatch-n (list [:do :all] [:three :of] [:these])}
81 | ```
82 | Note 1: The events supplied will be dispatched in the order provided.
83 | Note 2: nil events are ignored which means events can be added
84 | conditionally:
85 | ```clj
86 | {:dispatch-n (list (when (> 3 5) [:conditioned-out])
87 | [:another-one])}
88 | ```
89 |
90 | ***
91 | #### :deregister-event-handler
92 |
93 | removes a previously registered event handler. Expects either a single id
94 | (typically a keyword), or a seq of ids.
95 |
96 | usage:
97 | ```clj
98 | {:deregister-event-handler :my-id)}
99 | ```
100 | or:
101 | ```clj
102 | {:deregister-event-handler [:one-id :another-id]}
103 | ```
104 | ***
105 | #### :db
106 |
107 | reset! app-db with a new value. Expects a map.
108 |
109 | usage:
110 | ```clj
111 | {:db {:key1 value1 :key2 value2}}
112 | ```
113 |
114 | ***
115 |
116 | Previous: [Infographic: A re-frame Epoch](AnEpoch.md)
117 | Up: [Index](README.md)
118 | Next: [Infographic: Event Processing](EventHandlingInfographic.md)
119 |
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/docs/AnEpoch.md:
--------------------------------------------------------------------------------
1 | ## One re-frame Epoch
2 |
3 | The following graphic shows how, operationally, the six dominoes play out, over time, within the browser.
4 |
5 |
6 |
7 | ***
8 |
9 | Previous: [First Code Walk-Through](CodeWalkthrough.md)
10 | Up: [Index](README.md)
11 | Next: [The API](API.md)
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/docs/App-Structure.md:
--------------------------------------------------------------------------------
1 | ## Simpler Apps
2 |
3 | To build a re-frame app, you:
4 | - design your app's data structures (data layer)
5 | - write Reagent view functions (domino 5)
6 | - write event handler functions (control layer and/or state transition layer, domino 2)
7 | - write subscription functions (query layer, domino 4)
8 |
9 | For simpler apps, you should put code for each layer into separate files:
10 | ```
11 | src
12 | ├── core.cljs <--- entry point, plus history, routing, etc
13 | ├── db.cljs <--- schema, validation, etc (data layer)
14 | ├── views.cljs <--- reagent views (view layer)
15 | ├── events.cljs <--- event handlers (control/update layer)
16 | └── subs.cljs <--- subscription handlers (query layer)
17 | ```
18 |
19 | For a living example of this approach, look at the [todomvc example](https://github.com/day8/re-frame/tree/master/examples/todomvc).
20 |
21 | *No really, you should absolutely look at the [todomvc example](https://github.com/day8/re-frame/tree/master/examples/todomvc) example, as soon as possible. It contains all sorts of tips.*
22 |
23 | ### There's A Small Gotcha
24 |
25 | If you adopt this structure, there's a gotcha.
26 |
27 | `events.cljs` and `subs.cljs` will never be `required` by any other
28 | namespaces. To the Google Closure dependency mechanism, it appears as
29 | if these two namespaces are not needed and it doesn't load them.
30 |
31 | And, if the namespaces are not loaded, the registrations in these namespaces will
32 | never happen. And, then you'll be staring at your running app very
33 | puzzled about why none of your events handlers are registered.
34 |
35 | Once you twig to what's going on, the solution is easy. You must
36 | explicitly `require` both namespaces, `events` and `subs`, in your `core`
37 | namespace. Then they'll be loaded and the registrations (`reg-sub`, `reg-event-fx`,
38 | etc) will occur as that loading happens.
39 |
40 | ## Larger Apps
41 |
42 | Assuming your larger apps have multiple "panels" (or "views") which are
43 | relatively independent, you might use this structure:
44 | ```
45 | src
46 | ├── core.cljs <--- entry point, plus history, routing, etc
47 | ├── panel-1
48 | │ ├── db.cljs <--- schema, validation, etc (data layer)
49 | │ ├── subs.cljs <--- subscription handlers (query layer)
50 | │ ├── views.cljs <--- reagent components (view layer)
51 | │ └── events.cljs <--- event handlers (control/update layer)
52 | ├── panel-2
53 | │ ├── db.cljs <--- schema, validation. etc (data layer)
54 | │ ├── subs.cljs <--- subscription handlers (query layer)
55 | │ ├── views.cljs <--- reagent components (view layer)
56 | │ └── events.cljs <--- event handlers (control/update layer)
57 | .
58 | .
59 | └── panel-n
60 | ```
61 |
62 | If you follow this structure you should probably use namespaced keywords instead of simple keywords.
63 |
64 | This gives the ability to encapsulate the state of each "panel" and ensure you don't get any conflicts.
65 |
66 |
67 | Suppose for example that in your panel you want to store a value `x` in the db, if you want to use
68 | namespaced keywords you the event handler and subscription will look like this:
69 |
70 | ```clj
71 | (rf/reg-event-db
72 | ::set-x
73 | (fn [db [_ value]]
74 | (assoc db ::x value)))
75 |
76 | (rf/reg-sub
77 | ::x
78 | (fn [db _]
79 | (get db ::x)))
80 | ```
81 |
82 | If you want to dispatch that even you have two options, either:
83 |
84 | ```clj
85 | (require [project.panel.handlers :as handlers])
86 |
87 | (rf/dispatch [::handlers/set-x 100])
88 | ```
89 |
90 | or:
91 |
92 | ```clj
93 | (rf/dispatch [:project.panel.handlers/set-x 100])
94 | ```
95 |
96 | Where the first option might be preferrable since it ensures you require the handlers file and saves you from the possibility of typos.
97 |
98 | ## I Want Real Examples!
99 |
100 | Maybe look here:
101 | https://github.com/day8/re-frame/blob/master/docs/External-Resources.md#examples-and-applications-using-re-frame
102 |
103 | ***
104 |
105 | Previous: [Correcting a wrong](SubscriptionsCleanup.md)
106 | Up: [Index](README.md)
107 | Next: [Navigation](Navigation.md)
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/docs/ApplicationState.md:
--------------------------------------------------------------------------------
1 | ## Application State
2 |
3 |
4 |
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...
6 |
7 | ### The Big Ratom
8 |
9 | re-frame puts all your application state into one place, which is
10 | called `app-db`.
11 |
12 | Ideally, you will provide a spec for this data-in-the-one-place,
13 | [using a powerful and leverageable schema](http://clojure.org/about/spec).
14 |
15 | Now, this advice is not the slightest bit controversial for 'real' databases, right?
16 | You'd happily put all your well-formed data into PostgreSQL.
17 |
18 | But within a running application (in memory), there can be hesitation. If you have
19 | a background in OO, this data-in-one-place
20 | business is a really, really hard one to swallow. You've
21 | spent your life breaking systems into pieces, organised around behaviour and trying
22 | to hide state. I still wake up in a sweat some nights thinking about all
23 | that Clojure data lying around exposed and passive.
24 |
25 | But, as Fogus reminds us, data at rest is quite perfect.
26 |
27 | In re-frame, `app-db` is one of these:
28 | ```clj
29 | (def app-db (reagent/atom {})) ;; a Reagent atom, containing a map
30 | ```
31 |
32 | Although it is a `Reagent atom` (hereafter `ratom`), I'd encourage
33 | you to think of it as an in-memory database. It will contain structured data.
34 | You will need to query that data. You will perform CRUD
35 | and other transformations on it. You'll often want to transact on this
36 | database atomically, etc. So "in-memory database"
37 | seems a more useful paradigm than plain old map-in-atom.
38 |
39 | Further Notes:
40 |
41 | 1. `app-state` would probably be a more accurate name, but I choose `app-db` instead because
42 | I wanted to convey the in-memory database notion as strongly as possible.
43 | 2. In the documentation and code, I make a distinction between `app-db` (the `ratom`) and
44 | `db` which is the (map) `value` currently stored **inside** this `ratom`. Be aware of that naming as you read code.
45 | 3. re-frame creates and manages an `app-db` for you, so
46 | you don't need to declare one yourself (see the [the first FAQ](FAQs/Inspecting-app-db.md) if you want
47 | to inspect the value it holds).
48 | 4. `app-db` doesn't actually have to be a `ratom` containing a map. It could, for example,
49 | be a [datascript database](https://github.com/tonsky/datascript). In fact, any database which
50 | can signal you when it changes would do. We'd love! to be using [datascript database](https://github.com/tonsky/datascript) - so damn cool -
51 | but we had too much data in our apps. If you were to use it, you'd have to tweak re-frame a bit and use [Posh](https://github.com/mpdairy/posh).
52 |
53 |
54 | ### The Benefits Of Data-In-The-One-Place
55 |
56 | 1. Here's the big one: because there is a single source of truth, we write no
57 | code to synchronise state between many different stateful components. I
58 | cannot stress enough how significant this is. You end up writing less code
59 | and an entire class of bugs is eliminated.
60 | (This mindset is very different to OO which involves
61 | distributing state across objects, and then ensuring that state is synchronised, all the while
62 | trying to hide it, which is, when you think about it, quite crazy ... and I did it for years).
63 |
64 | 2. Because all app state is coalesced into one atom, it can be updated
65 | with a single `reset!`, which acts like a transactional commit. There is
66 | an instant in which the app goes from one state to the next, never a series
67 | of incremental steps which can leave the app in a temporarily inconsistent, intermediate state.
68 | Again, this simplicity causes a certain class of bugs or design problems to evaporate.
69 |
70 | 3. The data in `app-db` can be given a strong schema
71 | so that, at any moment, we can validate all the data in the application. **All of it!**
72 | We do this check after every single "event handler" runs (event handlers compute new state).
73 | And this enables us to catch errors early (and accurately). It increases confidence in the way
74 | that Types can increase confidence, only [a good schema can potentially provide more
75 | **leverage** than types](https://www.youtube.com/watch?v=nqY4nUMfus8).
76 |
77 | 4. Undo/Redo [becomes straight forward to implement](https://github.com/day8/re-frame-undo).
78 | It is easy to snapshot and restore one central value. Immutable data structures have a
79 | feature called `structural sharing` which means it doesn't cost much RAM to keep the last, say, 200
80 | snapshots. All very efficient.
81 | For certain categories of applications (eg: drawing applications) this feature is borderline magic.
82 | Instead of undo/redo being hard, disruptive and error prone, it becomes trivial.
83 | **But,** many web applications are not self contained
84 | data-wise and, instead, are dominated by data sourced from an authoritative, remote database.
85 | For these applications, re-frame's `app-db` is mostly a local caching
86 | point, and being able to do undo/redo its state is meaningless because the authoritative
87 | source of data is elsewhere.
88 |
89 | 5. The ability to genuinely model control via FSMs (discussed later).
90 |
91 | 6. The ability to do time travel debugging, even in a production setting. More soon.
92 |
93 |
94 | ### Create A Leveragable Schema
95 |
96 | You need to create a [spec](http://clojure.org/about/spec) schema for `app-db`. You want that leverage.
97 |
98 | Of course, that means you'll have to learn [spec](http://clojure.org/about/spec) and there's
99 | some overhead in that, so maybe, just maybe, in your initial experiments, you can
100 | get away without one. But not for long. Promise me you'll write a `spec`. Promise me. Okay, good.
101 |
102 | Soon we'll look at the [todomvc example](https://github.com/day8/re-frame/tree/master/examples/todomvc)
103 | which shows how to use a spec. (Check out `src/db.cljs` for the spec itself, and then in `src/events.cljs` for
104 | how to write code which checks `app-db` against this spec after every single event has been
105 | processed.)
106 |
107 | Specs are potentially more leveragable than types. This is a big interesting idea which is not yet mainstream.
108 | Watch how:
109 | https://www.youtube.com/watch?v=VNTQ-M_uSo8
110 |
111 | Also, watch the mighty Rich Hickey (poor audio):
112 | https://vimeo.com/195711510
113 |
114 | ### How do I inspect it?
115 |
116 | See [FAQ #1](FAQs/Inspecting-app-db.md)
117 |
118 | ***
119 |
120 | Previous: [This Repo's README](../README.md)
121 | Up: [Index](README.md)
122 | Next: [First Code Walk-Through](CodeWalkthrough.md)
123 |
124 |
125 |
126 |
127 |
128 |
--------------------------------------------------------------------------------
/docs/EPs/001-CaptureHandlerMetadata.md:
--------------------------------------------------------------------------------
1 | ## EP 001 - Better Capture Of Handler Metadata
2 |
3 | > Status: Drafting. May be incoherent and/or wrong. Don't read.
4 |
5 | ### Abstract
6 |
7 | This EP proposes changes to the way re-frame handlers are registered,
8 | to allow for the capture of richer handler metadata.
9 | These changes also lay the groundwork for tooling advances, and EPs to follow.
10 |
11 |
12 | ### Background
13 |
14 | re-frame's API currently includes 7 functions for registering handlers:
15 | - event: `reg-event-db`, `reg-event-fx` and `reg-event-ctx`
16 | - subscription: `reg-sub` and `reg-sub-raw`
17 | - effects: `reg-fx`
18 | - coeffects: `reg-cofx`
19 |
20 | Two others are on the drawing board:
21 | - `reg-view`
22 | - `reg-interceptor`
23 |
24 | So, there are potentially 9 `kinds` of handlers.
25 |
26 | Internally, re-frame manages registered handlers in a `registrar`, which is a two-level map,
27 | keyed at the first level by the `kind` of handler and at the second level by the (keyword)
28 | `id` of the handler. The leaf values are the handler functions themselves.
29 |
30 |
31 | ## Introduction
32 |
33 | This EP proposes that:
34 | 1. all current registration functions in the API be superseded by a new single macro `reg`
35 | 2. the leaf nodes of the `registrar`, which are currently the handler functions themselves,
36 | become instead a map of values related to the handler,
37 | including a doc string, the file/line where defined, specs, etc, and, of course,
38 | the handler itself.
39 |
40 |
41 | ## Motivations
42 |
43 | There's pressure from multiple directions to collect and retain more metadata about handlers:
44 | - tickets like [#457](https://github.com/day8/re-frame/issues/457) want docstrings for handlers
45 | - adding specs for events, so they can be checked at dev time
46 | - when re-frame becomes less of a framework and more of a library, handlers might
47 | need be "grouped" into "packages". So "package" information about handlers need to be supplied and retained.
48 | - Tooling support - we'd like `re-frame-10x` to actively help programmers when they are learning a
49 | new code base. That's one of [the four stated goals](https://github.com/day8/re-frame-10x#helps-me-how).
50 | Ideally, re-frame would be capable of providing tooling with "a complete
51 | inventory" of an app's handlers, along with useful
52 | metadata on each handles. When an event is processed, the audit trail of
53 | handlers involved should be rich with information.
54 |
55 | ## Macro
56 |
57 | As part of the retained handler metadata, we'd like to automatically capture
58 | source code coordinates, like namespace and line number.
59 | To make this happen, a macro will need to be introduced for registration, and that's a shift in
60 | approach because, until now, macros have been manfully resisted.
61 |
62 | Introducing docstrings into registrations also encourages
63 | a macro solution because docstrings should be removed from
64 | production builds.
65 |
66 | ## Method
67 |
68 | A new macro `reg` will become the method
69 | of registering handlers. The existing 7 registration functions
70 | will ultimately be deprecated.
71 |
72 | `reg` will take one argument, a map, which captures all aspects of
73 | the handler.
74 |
75 | ## Examples
76 |
77 | Previously, `reg-event-db` was used like this:
78 | ```clj
79 | (rf/reg-event-db
80 | :event-id
81 | (fn [db event]
82 | (assoc db :some :thing)))
83 | ```
84 |
85 | now, use `reg` would be used like this:
86 | ```clj
87 | (rf/reg
88 | {:kind :event-db
89 | :id :event-id
90 | :fn (fn [db event]
91 | (assoc db :some :thing))})
92 | ```
93 |
94 | The map argument must contain the keys `:kind`, `:id` and `:fn`,
95 | with other valid keys being dependent on the `kind` of
96 | handler being registered.
97 |
98 | The value `:kind` can be one these 7 (mapping to 7 existing `reg-*` functions):
99 | - `:event-db` `:event-fx` `:event-ctx`
100 | - `:sub` `:sub-raw`
101 | - `:fx`
102 | - `:cofx`
103 |
104 | Optionally, for all `kinds` of handlers the
105 | the map can also have these additional keys:
106 | - `:doc` a doc string
107 | - `:ns` the namespace where the handler was registered
108 | - `:line` line number where the handler was registered
109 | - `:file` the name file where the handler was registered
110 |
111 | In a dev build, the `reg` macro will supply the final 3 (source code coordinates),
112 | if not explicitly supplied in the map.
113 |
114 | In a production build, the `:doc` string will be elided, so we do not
115 | appear in the final source code at all.
116 |
117 | The key `:pkg` is reserved for future use, and might eventually indicate the
118 | "package" to which this handler belongs. See EP 002.
119 |
120 | Other keys: XXX
121 | - `:cept` for interceptors (when registering events)
122 | - `:ins` for input signals (when registering subscriptions)
123 | - `:ret` for return spec (subscriptions and events)
124 | - `:spec` for event spec (when registering events) ??? Too much ??
125 |
126 | XXX I'm not entirely happy about using short names like `:cept`. But, then
127 | again, there's the aesthetics of formatting the code and lining things up.
128 |
129 | XXX could have a `:cofx` key for event handlers to make them more explicit.
130 |
131 | ### Multiple Registrations
132 |
133 | The argument to `reg` can also be a vector of maps to allow
134 | for multiple handlers to be registered at once:
135 |
136 | ```clj
137 | (rf/reg
138 | [{:kind :event-db ...}
139 | {:kind :event-db ...}
140 | {:kind :event-fx ...}
141 | {:kind :sub ...])
142 | ```
143 |
144 | XXX maybe not needed. Provide the most minimal API? Then let towers of abstraction be built on top.
145 |
146 | ### Registrar
147 |
148 | Each entry stored in the registrar will become a map instead of just a handler.
149 |
150 | Map keys:
151 | - `:kind` - somehow redundant
152 | - `:id`
153 | - `:doc`
154 | - `:line`
155 | - `:ns`
156 | - `:doc`
157 | - `:fn` the handler
158 |
159 | XXX look into reg-sub
160 |
161 | ### Backwards Compatibility
162 |
163 | XXX
164 |
165 | ## Issues/Questions/Todos
166 |
167 | - XXX implications for Cursive - it currently special-cases re-frame registration function -- give ColinF a heads up??
168 | - XXX Dear God, consider changes to documentation/tutorials
169 | - XXX means giving up syntax sugar for reg-sub ?
170 | - XXX any format for `:doc` for display in HTML? Or just texual.
171 |
172 |
173 |
174 |
--------------------------------------------------------------------------------
/docs/EPs/003-ReusableComponents.md:
--------------------------------------------------------------------------------
1 | ## EP 003 - Enabling Creation Of Reusable Components
2 |
3 | > Status: Placeholder. Don't bother reading yet.
4 |
5 | ### Abstract
6 |
7 | This EP proposes changes to facilitate easier use of React's Context feature.
8 | XXX to make it easier to write more complex Reusable components.
9 |
10 | ### Background
11 |
12 | React components form a tree, with values being passed down
13 | through the hierarchy in the form of `props`. All very functional.
14 |
15 | Except there are some problems:
16 | - it can be a PITA to pass every little bit of data through many, many layers.
17 | Manual and time consuming.
18 | - often we don't want to burden intermediate layers with knowledge about
19 | what leaf nodes needed. That kind of "unnecessary knowing" leads to
20 | various kinds of fragility.
21 | - if we are using someone else's layout components, we may have not have
22 | control over what they pass to children.
23 |
24 | [Algebraic Effects](http://math.andrej.com/eff/) are intended to help solve
25 | these kinds of problems in a functional way, but that's not our world.
26 |
27 | The solution available in React is called `Context`. It is a mechanism for allowing
28 | data to be "shared" globally within a given tree of React components
29 | (without it needing for it to be passed/threaded through all layers of that tree).
30 |
31 | [React's context docs are here](https://reactjs.org/docs/context.html).
32 |
33 | ### Components
34 |
35 | `re-com` is a library which supplies reusable Reagent widgets. And `widgets`,
36 | like a datepicker, are the simplest kind of components.
37 |
38 | `re-com` components are reusability because they take `an input stream` of data
39 | and they
40 |
41 | achieves reusablity by passing in values and suppling callbacks. This works at
42 | the level of simple widgets.
43 |
44 | But re-frame components need to subscribe and dispatch.
45 |
46 | XXX talk about `dispatch back` rather than `callback`
47 |
48 | XXX need to identify the part of `app-db` on which `event handlers` and `subscriptions` should operate.
49 |
50 |
51 |
--------------------------------------------------------------------------------
/docs/EPs/004-ViewRegistration.md:
--------------------------------------------------------------------------------
1 | ## EP 003 - View Registration
2 |
3 | > Status: Placeholder. Only scribbles. Don't read yet.
4 |
5 |
6 | ### Abstract
7 |
8 | Broadbrush:
9 | - views will be registered via a new `re-frame.core/def-view` function
10 | - like other re-frame registration functions, `def-view` associates a `keyword` with a (reagent) render function
11 | - the registered view keyword (eg: `:my-view`) can be used in hiccup to identify the renderer. eg: `[:my-view "Hello world"]`
12 | - `def-view` allows various values to be `injected` as args into the view render
13 | - see https://github.com/reagent-project/reagent/issues/362
14 |
15 | Why:
16 | - removing (render) functions from hiccup will make hiccup even more data oriented. Symptoms include helping with various state machine ideas.
17 | - injection of `dispatch` and `subscribe` will help view functions to be slightly more pure. `dispatch` still kinda a problem.
18 | - ijection of `context` which will help with "multiple re-frame apps on the one page" problem
19 |
20 | What might need to be injected (as args) into a view:
21 |
22 | - `subscribe` and `dispatch`
23 | - a `frame` supplied via `context` (subscribe and dispatch obtained from frame)
24 | - other context: data from higher in the DOM tree
25 | - annimation? CSS ?
26 |
27 | XXX searches up the DOM heirarchy looking for a `frame` context then extracts dispatch and subscribe. Sounds inefficient.
28 |
29 | ### Code Doodle #1
30 |
31 | Associate the keyword `:my-view-id ` with a renderer using `def-view`:
32 | ```clj
33 | (def-view
34 | :my-view-id
35 |
36 | ;; indicate what `context` is required
37 | [:dispatch :subscribe :context XXX]
38 |
39 | ;; the renderer
40 | ;; last argument `context` is a map of:
41 | ;; - `:subs` - a vector of subscription values?
42 | ;; - :dispatch and :subscribe
43 | ;; - :context - a vector of context values
44 | ;;
45 | (fn [a-str context]
46 | (let [XXXX]
47 | )))
48 | ```
49 |
50 | Use of `:my-view-id `:
51 | ```clj
52 | [:my-view-id "Hello"]
53 | ```
54 |
55 | ### Code Doodle #2
56 |
57 | Associate the keyword `:my-view-id ` with a renderer using `def-view`:
58 | ```clj
59 | (def-view
60 | :my-view-id
61 |
62 | ;; injection function
63 | ;; indicate what subscriptions we wish to obtain
64 | ;; obtain a dispatch for use
65 | ;; get the context id if you want to
66 | ;;
67 | :subscriptions
68 | (fn [_ id]
69 | {:subs [[:item ]]
70 | :context ["name1", "name2")})
71 |
72 |
73 | ;; the renderer
74 | ;; last argument `ins` is a map of:
75 | ;; - `:subs` - a vector of subscription values?
76 | ;; - :dispatch and :subscribe
77 | ;; - :context - a vector of context values
78 | ;;
79 | (fn [a-str ins]
80 | (let [XXXX]
81 | )))
82 | ```
83 |
84 | Use of `:my-view-id `:
85 | ```clj
86 | [:my-view-id "Hello"]
87 | ```
88 |
89 | ### Code Doodle #3
90 |
91 | `[:something arg1 arg2]`
92 |
93 | ```clj
94 | (def-view
95 | :something
96 | (fn [arg1 arg2]
97 | ;; obtain dispatch and subscription
98 | ;; obtain a subscription ot two
99 | ;; add a key on the component
100 | (fn [arg1 arg2]
101 | ))
102 |
103 | ```
104 |
105 | ## Misc Notes
106 |
107 | - reagent hiccup will be changed/monkey-patched so that views can be identified by keyword
108 | - Views are the leaves of the signal graph. They need to subscribe and dispatch.
109 | - how to obtain other pieces of `context` (beyond the current frame)
110 |
111 |
112 | XXX There's a nasty problem with frames and subscriptions. How does the signal function know what frame to create new subscriptions against???
113 |
114 | ## Usage
115 |
116 |
117 |
--------------------------------------------------------------------------------
/docs/EPs/005-StateMachines.md:
--------------------------------------------------------------------------------
1 | ## EP 003 - Finite State Machines
2 |
3 | > Status: Placeholder. Don't bother reading yet.
4 |
5 | ### Abstract
6 |
7 | Colloquial: I'd like re-frame to have a better story for programming in FSMs. I want to
8 | represent "Higher Order Behaviour" which currently gets "smeared" across
9 | multiple event handlers.
10 |
11 | ### Introduction
12 |
13 | Many **high level** aspects of SPAs are best modelled explicitly as an FSM.
14 | First, an app must gather X from the user, but if they press cancel, then return to
15 | showing Y, but otherwise move on to do activity Z. It is quite natural to model such overall
16 | decisions and the UIs involved as an FSM.
17 |
18 | Many **low level** aspects of SPAs are best modelled explicitly as FSM too.
19 | The simple act of GETing from a server involves various
20 | states, including waiting, and failed, and timed-out, and retrying and succeeded.
21 |
22 | BUT you need the power of a fully expressive state machine.
23 | You need orthogonal state, guards, hierarchical state, history, etc.
24 | Back in 1987, Harel identified the set of features required - anything less
25 | and your tool will not be expressive enough to do the job.
26 | Harel also insisted that statecharts was a visual
27 | formalism, so tooling is also important.
28 |
29 | ### Why?
30 |
31 | > Beware of the Turing tar-pit in which everything is possible but nothing of interest is easy.
32 | > -- Alan Perlis
33 |
34 | State machines are appealing because:
35 | - they constrain you (vs the full Turing tar-pit). Just because you *can* wield
36 | immense power doesn't mean you should.
37 | - they force you to think through and structure a problem. This process helps to flush out the corner cases.
38 | - they make explicit certain important assumptions which are otherwise hidden in a thicket of conditionals.
39 |
40 |
41 | Also, Why Developers Never Use State Machines
42 | https://www.skorks.com/2011/09/why-developers-never-use-state-machines/
43 |
44 | > "The strength of JavaScript is that you can do anything. The weakness is that you will."
45 | > - Reg Braithwaite
46 |
47 | ### How?
48 |
49 | Technical elements:
50 | - a way to register:
51 | - a state machine specification against a `machine-id`
52 | - the specification will be a data structure, listing states, transitions, etc
53 | - a way to create an instance of a registered state machine
54 | - args: machine-id, id for this particular instance
55 | - data for machine instance will be stored in `app-db` (at `:machines` or a configurable place?)
56 | - a way to trigger
57 | - the `id` of the state machine targeted
58 | - the trigger
59 | - the trigger args
60 | - trigger causes:
61 | - state transition
62 | - an action fn to be called which produces `:effects`
63 | - UI changes. See EP on `reg-view` which will make it much easier to describe UI in machine data structure
64 | - a way to destroy an instance
65 |
66 | ### Misc Notes
67 |
68 | Events model user intent, not implementation details.
69 |
70 | So, we DO NOT want events that talk about FSM or triggers etc because that's an implementation detail.
71 |
72 | Instead, we want
73 |
74 |
75 | But event handlers should know about XXX
76 |
77 |
78 | ### Triggers
79 |
80 |
81 | Types of triggers:
82 | 1. External (from the user, websocket)
83 | 2. Data - something about `app-db` has changed
84 |
85 | ### Implementation
86 |
87 | What if we didn't even use FSM and used Behaviour Trees instead?
88 | Behaviour trees are more composable. A better match for a data-oriented design.
89 |
90 |
91 | Links And Notes:
92 |
93 | - [statecharts](https://statecharts.github.io/)
94 | - [THE PRECISE SEMANTICS OF UML STATE MACHINES](https://www.omg.org/spec/PSSM/1.0/Beta1/PDF)
95 | - [David Khourshid - Infinitely Better UIs with Finite Automata](https://www.youtube.com/watch?v=VU1NKX6Qkxc) also [written](https://css-tricks.com/robust-react-user-interfaces-with-finite-state-machines/)
96 | - [Statecharts: Updating UI state](https://medium.com/@lmatteis/statecharts-updating-ui-state-767052b6b129)
97 | - [https://statecharts.github.io/](https://statecharts.github.io/)
98 | - [Harel Paper](http://www.inf.ed.ac.uk/teaching/courses/seoc/2005_2006/resources/statecharts.pdf)
99 | - [setState Machine Speaker Deck by Michele Bertoli](https://speakerdeck.com/michelebertoli/setstate-machine)
100 |
101 |
102 |
103 | Previously CLJS :
104 | - https://github.com/jebberjeb/reframe-fsm
105 | - https://github.com/protocol55/re-state
106 | - http://blog.cognitect.com/blog/2017/8/14/restate-your-ui-creating-a-user-interface-with-re-frame-and-state-machines
107 |
108 |
109 | Other Attempts:
110 | - [Fractal UI components using snabbdom, Harel statecharts, and event emitters](https://github.com/jayrbolton/snabbdom-statechart-components)
111 |
112 | - [BT 101 – Behavior Trees grammar basics](http://www.craft.ai/blog/bt-101-behavior-trees-grammar-basics/)
113 | - [Understanding Behavior Trees](http://aigamedev.com/open/article/bt-overview/)
114 | - [behavior3js github repo](https://github.com/behavior3/behavior3js)
115 | - [Understanding the Second Generation of Behavior Trees (video)](https://www.youtube.com/watch?v=n4aREFb3SsU)
116 | - [10 Reasons the Age of Finite State Machines is Over](http://aigamedev.com/open/article/fsm-age-is-over/)
117 | - [Parameterizing Behavior Trees](https://people.cs.umass.edu/~fmgarcia/Papers/Parameterizing%20Behavior%20Trees.pdf)
118 | - [Behavior Trees in Robotics and AI](https://arxiv.org/pdf/1709.00084.pdf)
119 |
120 | TLA+
121 | - [** Three Approximations - includes SAM](https://dzone.com/articles/the-three-approximations-you-should-never-use-when)
122 | - [State Machines and Computing](https://www.ebpml.org/blog2/index.php/2015/01/16/state-machines-and-computing)
123 | - [SAM – the State-Action-Model pattern](https://www.ebpml.org/blog15/2015/06/sam-the-state-action-model-pattern/)
124 | - [TLA Intro](https://lamport.azurewebsites.net/tla/tla-intro.html)
125 | - [Computation and State Machines - Leslie Lamport](https://www.microsoft.com/en-us/research/publication/computation-state-machines/)
126 | - [SAM Pattern in JS](http://sam.js.org/)
127 | -
128 |
129 |
130 |
--------------------------------------------------------------------------------
/docs/EPs/README.md:
--------------------------------------------------------------------------------
1 | EP
2 |
3 | This directory contains re-frame "Enhancement Proposals" (EPs), one per file.
4 |
5 | ## Status
6 |
7 | Each EP proceeds through the following stages:
8 | - **Placeholder** - skeleton at best. One day someone might write something.
9 | - **Drafting** - some writing and thinking has been done, but it may be incoherent and/or wrong.
10 | - **UnderReview** - ready for general discussion in a specifically created (repo) Issue.
11 | - **Accepted** or **Rejected**
12 | - **Released**
13 |
14 |
15 | ## List
16 |
17 | - [EP 001 - Capture Handler Metadata](001-CaptureHandlerMetadata.md) - Drafting
18 | - [EP 002 - Multiple re-frame Instances](002-ReframeInstances.md) - Drafting
19 | - [EP 003 - Reusable Components Via context](003-ReusableComponents.md) - Placeholder
20 | - [EP 004 - View Registration](004-ViewRegistration.md) - Placeholder
21 | - [EP 005 - State Machines](005-StateMachines.md) - Placeholder
22 |
23 |
--------------------------------------------------------------------------------
/docs/EventHandlingInfographic.md:
--------------------------------------------------------------------------------
1 |
2 | ## Event Handling Infographics
3 |
4 | Three diagrams are provided below:
5 | - a beginner's romp
6 | - an intermediate schematic depiction
7 | - an advanced, full detail rendering
8 |
9 | They should be reviewed in conjunction with the written tutorials.
10 |
11 |
12 |
13 | ***
14 |
15 | Previous: [The API](API.md)
16 | Up: [Index](README.md)
17 | Next: [Effectful Handlers](EffectfulHandlers.md)
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/docs/FAQs/CatchingEventExceptions.md:
--------------------------------------------------------------------------------
1 | ### Question
2 |
3 | How can I detect exceptions in Event Handlers?
4 |
5 | ### Answer
6 |
7 | A suggested solution can be found in [this issue](https://github.com/day8/re-frame/issues/231#issuecomment-249991378).
8 |
9 | ***
10 |
11 | Up: [FAQ Index](README.md)
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/docs/FAQs/DB_Normalisation.md:
--------------------------------------------------------------------------------
1 | ### Question
2 |
3 | `app-db` contains a `map`. How do I store normalised data in a `map`,
4 | bettering mirroring the structure in my server-side database?
5 |
6 | ### Answer
7 |
8 | These libraries might be interesting to you:
9 | - [compound](https://github.com/riverford/compound)
10 | - [SubGraph](https://github.com/vimsical/subgraph)
11 | - [pull](https://github.com/juxt/pull)
12 |
13 | See also [this comment](https://github.com/day8/re-frame/issues/304#issuecomment-269620609).
14 |
15 | If you want to go whole hog and ditch `app-db` and embrace DataScript,
16 | then you might find [re-posh](https://github.com/denistakeda/re-posh) interesting.
17 |
18 |
19 | ***
20 |
21 | Up: [FAQ Index](README.md)
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/docs/FAQs/DoINeedReFrame.md:
--------------------------------------------------------------------------------
1 | ### Question
2 |
3 | Reagent looks terrific. So, why do I need re-frame? What benefit
4 | is there in the extra layers and conceptual overhead it brings?
5 |
6 | ### Answer
7 |
8 | Yes, we agree, Reagent is terrific. We use it enthusiastically ourselves. And, yes, we'd agree that if your application
9 | is small and simple, then standalone Reagent is a reasonable choice.
10 |
11 | But it does only supply the V part of the MVC triad. As your application
12 | gets bigger and more complicated, you *will* need to find solutions to
13 | questions in the M and C realms.
14 |
15 | Questions like "where do I put control logic?".
16 | And, "how do I store and update state?".
17 | And, "how should new websocket packets be communicated with the broader app"? Or GET failures?
18 | And, "how do I put up a spinner
19 | when waiting for CPU intensive computations to run, while allowing the user to press Cancel?"
20 | How do I ensure efficient view updates? How do I write my control logic in a way that's testable?
21 |
22 | These questions accumulate.
23 |
24 | Reagent, by itself, provides little guidance and, so, you'll need to
25 | design your own solutions. Your choices will also accumulate and,
26 | over time, they'll become baked into your codebase.
27 |
28 | Now, any decision which is hard to revisit later is an architectural decision -
29 | "difficult to change later" is pretty much the definition of "architecture". So,
30 | as you proceed, baking your decisions into your codebase, you will be
31 | incrementally growing an architecture.
32 |
33 | So, then, the question is this: is your architecture better than re-frame's? Because
34 | that's what re-frame gives you ... an architecture ... solutions to the
35 | various challenges you'll face when developing your app, and mechanism for implementing
36 | those solutions.
37 |
38 | Now, in response, some will enthusiastically say "yes, I want to grow my own
39 | architecture. I like mine!". And fair enough - it can be an interesting ride!
40 |
41 | Problems arise ONLY when this process is not conscious and purposeful. It is a
42 | credit to Reagent that you can accelerate quickly and get a bunch of enjoyable
43 | early wins. But, equally, that acceleration can have you off the road
44 | in a ditch because of the twists and turns on the way to a larger application.
45 |
46 | I've had many people (20?) privately say to me that's what happened to them.
47 | And that's pretty much the reason for this FAQ - this happens a bit too often
48 | and there's been a bit too much pain.
49 |
50 | So, my advice is ... if your application is a little more complicated,
51 | be sure to make a conscious choice around architecture. Don't think
52 | "Reagent is all I need", because it isn't. One way or
53 | another you'll be using "Reagent + a broader architecture".
54 |
55 | ### Example Choices Made By re-frame
56 |
57 | 1. Events are cardinal. Nothing happens without an event.
58 | 2. Events are data (so they they are loggable, and can be queued, etc).
59 | 3. Events are handled async (a critical decision. Engineered to avoid some `core.async` issues!).
60 | 4. For efficiency, subscriptions (reactions) should be layered and de-duplicated.
61 | 5. Views are never imperative or side effecting (best effort).
62 | 6. Unidirectional data flow only, ever.
63 | 7. Interceptors over middleware. Provide cross cutting concerns like logging and debugging.
64 | 8. Event handlers capture control and contain key code. Ensure purity via coeffects and effects.
65 | 9. All state is stored in one place.
66 | 10. State is committed-to transactionally, never incrementally or piecemeal.
67 |
68 | Hmm. I feel like I'm missing a few, but that's certainly an indicative list.
69 |
70 | re-frame is only about 750 lines of code. So it's value is much more in the honed
71 | choices it embodies (and documents), than the code it provides.
72 |
73 | ### What Reagent Does Provide
74 |
75 | Above I said:
76 | > Reagent, by itself, provides little guidance ...
77 |
78 | which is true but, it does provide useful building blocks. If you do want to create
79 | your own architecture, then be sure to check out Reagent's `track`, `reaction` and `rswap`.
80 |
81 | There's also other Reagent-based architectures like [keechma](https://github.com/keechma/keechma) and
82 | [carry](https://github.com/metametadata/carry) which make different choices - ones which may
83 | better suit your needs.
84 |
85 | ***
86 |
87 | Up: [FAQ Index](README.md)
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/docs/FAQs/FullStackReframe.md:
--------------------------------------------------------------------------------
1 | ### Question
2 |
3 | I'm interested in taking the re-frame concepts and applying them to
4 | my entire Client/Server stack.
5 |
6 | ### Short Answer
7 |
8 | You'll want to investigate CQRS (vs REST).
9 |
10 | ##### Notes
11 |
12 | 1. Perhaps watch [Bobby Calderwood's video](https://www.youtube.com/watch?v=B1-gS0oEtYc)?
13 | 2. Look at his [reference implementation](https://github.com/capitalone/cqrs-manager-for-distributed-reactive-services) or, perhaps, [this alternative](https://github.com/greywolve/calderwood).
14 | 4. Be aware that "Event Sourcing" often comes along for the ride
15 | with CQRS, but it doesn't have to. It adds complexity (Kafka?).
16 | Don't do it lightly. Maybe just use CQRS without ES?
17 | 5. If you do want Event Sourcing, then Kafka might be your friend,
18 | Greg Young might be your God and [Onyx](https://github.com/onyx-platform/onyx)
19 | may be useful.
20 |
21 | ### Further Related Links
22 |
23 | * Reactive PostgreSQL:
24 | https://yogthos.net/posts/2016-11-05-LuminusPostgresNotifications.html
25 | * Datalog All The Way Down:
26 | https://www.youtube.com/watch?v=aI0zVzzoK_E
27 |
28 |
29 | ***
30 |
31 | Up: [FAQ Index](README.md)
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/docs/FAQs/GlobalInterceptors.md:
--------------------------------------------------------------------------------
1 | ### Question
2 |
3 | Does re-frame allow me to register global interceptors? Ones which are included
4 | for every event handler?
5 |
6 | ### Short Answer
7 |
8 | No, nothing direct.
9 |
10 | ### Longer Answer
11 |
12 | It's easy to make happen.
13 |
14 | Let's assume you have an interceptor called `omni-ceptor` which you want
15 | automatically added to all your event handlers.
16 |
17 | You'd write a replacement for both `reg-event-db` and `reg-event-fx`, and get
18 | these replacements to automatically add `omni-ceptor` to the interceptor
19 | chain at registration time.
20 |
21 | Here's how to write one of these auto-injecting replacements:
22 | ```clj
23 | (defn my-reg-event-db ;; a replacement for reg-event-db
24 |
25 | ;; 2-arity with no interceptors
26 | ([id handler]
27 | (my-reg-event-db id nil handler))
28 |
29 | ;; 3-arity with interceptors
30 | ([id interceptors handler]
31 | (re-frame.core/reg-event-db ;; which uses reg-event-db
32 | id
33 | [omni-ceptor interceptors] ;; <-- inject `omni-ceptor`
34 | handler)))
35 | ```
36 |
37 | NB: did you know that interceptor chains are flattened and nils are removed?
38 |
39 | With this in place, you would always use `my-reg-event-db`
40 | instead of the standard `reg-event-db`:
41 | ```clj
42 | (my-reg-event-db
43 | :event-id
44 | (fn [db v]
45 | ...))
46 | ```
47 |
48 | And, hey presto, you'd have your `omni-ceptor` "globally" injected.
49 |
50 |
51 | ***
52 |
53 | Up: [FAQ Index](README.md)
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/docs/FAQs/Inspecting-app-db.md:
--------------------------------------------------------------------------------
1 | ### Question
2 |
3 | How can I inspect the contents of `app-db`? Perhaps from figwheel.
4 |
5 | ### Short Answer
6 |
7 | If at a REPL, inspect: `re-frame.db/app-db`.
8 |
9 | If at the js console, that's `window.re_frame.db.app_db.state`.
10 |
11 | If you want a visual browser of app-db, along with inspecting subpaths of app-db, and diffing changes, use [re-frame-10x](https://github.com/day8/re-frame-10x).
12 |
13 | You are [using cljs-devtools](https://github.com/binaryage/cljs-devtools), right?
14 | If not, stop everything ([unless you are using re-natal](https://github.com/drapanjanas/re-natal/issues/137)) and immediately make that happen.
15 |
16 | ### Better Answer
17 |
18 | Are you sure you need to?
19 |
20 | First, you seldom want to inspect all of `app-db`.
21 | And, second, inspecting via a REPL might be clumsy.
22 |
23 | Instead, you probably want to inspect a part of `app-db`. __And__ you probably want
24 | to inspect it directly in the GUI itself, not off in a REPL.
25 |
26 | Here is a useful technique from @escherize. Add something like this to
27 | the hiccup of your view ...
28 | ```clj
29 | [:pre (with-out-str (pprint @interesting))]
30 | ```
31 | This assumes that `@interesting` is the value (ratom or subscription)
32 | you want to observe (note the @ in front).
33 |
34 | `pprint` output is nice to read, but not compact. For a more compact view, do this:
35 | ```clj
36 | [:pre (pr-str @some-atom)] ;; NB: using pr-str instead of pprint
37 | ```
38 |
39 | If you choose to use `pprint` then you'll need to `require` it within the `ns` of your `view.cljs`:
40 | ```clj
41 | [cljs.pprint :refer [pprint]]
42 | ```
43 |
44 | Finally, combining the short and long answers, you could even do this:
45 | ```clj
46 | [:pre (with-out-str (pprint @re-frame.db/app-db))] ;; see everything!
47 | ```
48 | or
49 | ```clj
50 | [:pre (with-out-str (pprint (:part @re-frame.db/app-db)))] ;; see a part of it!
51 | ```
52 |
53 | You definitely have [clj-devtools](https://github.com/binaryage/cljs-devtools) installed now, right?
54 |
55 | ### Other Inspection Tools
56 |
57 | Another very strong tool is [re-Frisk](https://github.com/flexsurfer/re-frisk) which
58 | provides a nice solution for navigating and inspecting your re-frame data structures.
59 |
60 | @yogthos' [json-html library](https://github.com/yogthos/json-html) provides
61 | a slick presentation, at the expense of more screen real estate, and the
62 | need to include specific CSS.
63 |
64 | ***
65 |
66 | Up: [FAQ Index](README.md)
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/docs/FAQs/LoadOnMount.md:
--------------------------------------------------------------------------------
1 | ### Question
2 |
3 | How do I load the data for a view, on navigation to that view?
4 |
5 | ### Don't Do This
6 |
7 | The broader React community often uses a "load data on mount" approach. They
8 | collocate queries with view components and initiate these queries (via a GET?)
9 | within the View's componentDidMount lifecycle method. And then, later, perhaps they
10 | cleanup/stop any database polling in componentWillUnmount.
11 |
12 | This arrangement is not idiomatic for re-frame. Views are not imperative
13 | and they don't initiate database queries. Instead, views are simply a rendering of the
14 | current application state.
15 |
16 | Please read the further explanation in [PurelyFunctional.tv's writeup](https://purelyfunctional.tv/article/react-vs-re-frame/) under the heading "Reacters load data on mount".
17 |
18 | ### Do This Instead
19 |
20 | With re-frame, "imperative stuff" only ever happens because an event
21 | is dispatched.
22 |
23 | When the user clicks on a button or tab to change what is shown
24 | to them in the UI, an event is dispatched, and it is
25 | the associated event handler which will compute the
26 | effects of the user's request. It might:
27 | 1. change application state so the panel is shown
28 | 2. further change application state so that a "twirly busy" thing is shown
29 | 3. issue a database query or open a websocket
30 |
31 | Also, remember that events should model "user intent", like
32 | "I'd now like to view overdue items". Be sure to never model events like
33 | "load overdue items from database" because that's just a
34 | low level operation performed in the service of fulfilling
35 | the user's intent.
36 |
37 | There's a useful effect handler available for HTTP work:
38 | https://github.com/day8/re-frame-http-fx
39 |
40 | Look at the "Real World App" example for inspiration:
41 | https://github.com/jacekschae/conduit
42 |
43 | ### Do You Have A Redux Background?
44 |
45 | The reminder about the re-frame approach:
46 | 1. re-frame is event driven. It is events which move the system from one state to the next
47 | 2. events cause changes: perhaps to `app-db` or opening a web socket. These are the effects of the event
48 | 3. subscriptions simply deliver data
49 | 4. subscriptions are not imperative. They do not cause things to happen
50 | 5. only events cause things to happen and, even then, only via effects
51 | 6. views simply render the data delivered by subsciptions
52 | 7. views are not imperative. They do not cause things to happen
53 | 8. only events cause things to happen and, even then, only via effects
54 |
55 |
56 | ***
57 |
58 | Up: [FAQ Index](README.md)
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/docs/FAQs/Logging.md:
--------------------------------------------------------------------------------
1 | ### Question
2 |
3 | I use logging method X, how can I make re-frame use my method?
4 |
5 | ### Answer
6 |
7 | re-frame makes use of the logging functions: `warn`, `log`, `error`, `group` and `groupEnd`.
8 |
9 | By default, these functions map directly to the js/console equivalents, but you can
10 | override that by providing your own set or subset of these functions using
11 | `re-frame.core/set-loggers!` like this:
12 | ```clj
13 | (defn my-warn
14 | [& args]
15 | (post-warning-somewhere (apply str args)))
16 |
17 | (defn my-log
18 | [& args]
19 | (write-to-datadog (apply str args)))
20 |
21 | (re-frame.core/set-loggers! {:warn my-warn
22 | :log my-log
23 | ...})
24 | ```
25 |
26 | ***
27 |
28 | Up: [FAQ Index](README.md)
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/docs/FAQs/Null-Dispatched-Events.md:
--------------------------------------------------------------------------------
1 | ### Question
2 |
3 | If I `dispatch` a js event object (from a view), it is nullified
4 | by the time it gets to the event-handler. What gives?
5 |
6 | ```cljs
7 | :on-click (fn [event] (dispatch [:clicked event]))
8 | ```
9 |
10 | ### Short Answer
11 |
12 | If you want to `dispatch` a js event object to a re-frame
13 | event handler, you must call `(.persist event)` before the `dispatch`.
14 | React recycles events (using a pool), and re-frame event handlers
15 | run async. [Find out more here](https://facebook.github.io/react/docs/events.html)
16 |
17 |
18 | ### Longer Answer
19 |
20 | It is better to extract the salient details from the event
21 | and `dispatch` them, rather than the entire js event object. When you
22 | `dispatch` pure, simple ClojureScript data (ie. rather than js objects) testing
23 | and debugging will be easier.
24 |
25 | To put this point even more strongly again, think about it like this:
26 | - a DOM `on-click` `callback` might tell us "a button was clicked"
27 | - our application must then interpret that click. The click means
28 | the user wanted to achieve something. They had "intent".
29 | - it is this "intent" which should be captured in the re-frame `event`
30 | which is dispatched. It is this intent which the event handler must
31 | later facilitate.
32 |
33 |
34 | So, in summary, re-frame view functions should transform DOM events
35 | into re-frame `events` which capture user intent: "a button was clicked"
36 | becomes `user wants to delete item with id 42`
37 |
38 | As a result, philosophically, low-level DOM objects have no place in an event.
39 |
40 |
41 | ***
42 |
43 | Up: [FAQ Index](README.md)
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/docs/FAQs/PollADatabaseEvery60.md:
--------------------------------------------------------------------------------
1 | ### Question
2 |
3 | When the user switches to a particular panel, I'd like to start regularly polling my
4 | backend (database) - say every 60 seconds. And, then later, when the user switches
5 | away from that panel, I want to stop that polling.
6 |
7 | ### First, What Not To Do
8 |
9 | Please be sure to read [How Do I Load On Mount?](LoadOnMount.md)
10 |
11 | ### An Answer
12 |
13 |
14 | We'll create an effect. It will be general in nature.
15 |
16 | It will start and stop the timed/scheduled dispatch of an event.
17 | For this FAQ,
18 | we want an event dispatched every 60 seconds and each event will
19 | trigger a backend poll, but the effect we are about to create
20 | will be useful well beyond this narrow case.
21 |
22 | We'll be creating an `effect` called, say, `:interval`. So, event handlers
23 | will be returning:
24 | ```clj
25 | {:interval }
26 | ```
27 | So now we design the `` bit. It will be a data format (DSL) which
28 | allows an event handler to start and stop a regular event dispatch.
29 |
30 | To `:start` a regular dispatch, an event handler would return
31 | data in this format:
32 | ```clj
33 | {:interval {:action :start
34 | :id :panel-1-query ;; my id for this (so I can cancel later)
35 | :frequency 60000 ;; how many ms between dispatches
36 | :event [:panel-query 1]}} ;; what to dispatch
37 | ```
38 |
39 | And to later cancel the regular dispatch, an event handler would return this:
40 | ```clj
41 | {:interval {:action :cancel
42 | :id :panel-1-query}} ;; the id provided to :start
43 | ```
44 |
45 | With that design work done, let's now implement it by registering an
46 | `effect handler`:
47 | ```clj
48 | (re-frame.core/reg-fx ;; the re-frame API for registering effect handlers
49 | :interval ;; the effect id
50 | (let [live-intervals (atom {})] ;; storage for live intervals
51 | (fn [{:keys [action id frequency event]}] ;; the handler
52 | (if (= action :start)
53 | (swap! live-intervals assoc id (js/setInterval #(dispatch event) frequency)))
54 | (do (js/clearInterval (get @live-intervals id))
55 | (swap! live-intervals dissoc id))))
56 | ```
57 |
58 | You'd probably want a bit more error checking, but that's the (untested) sketch.
59 |
60 | ### A Side Note About Effect Handlers and Figwheel
61 |
62 | [Figwheel](https://github.com/bhauman/lein-figwheel) provides for the hot reloading of code, which
63 | is terrific.
64 |
65 | But, during development, as Figwheel is reloading code, effectful handlers, like the
66 | one above, can be get into a messed up state - existing timers might be lost (and
67 | become never-stoppable).
68 |
69 | Stateful things are grubby in the face of reloading, and all we can do is
70 | try to manage for it as best we can, on a case by case basis.
71 |
72 | One strategy is to put all your grubby effect handlers into their own
73 | separate namespace `effects.cljs` - one that isn't edited often, removing
74 | the trigger for a Figwheel reload.
75 |
76 | OR, you can code defensively for reloading, perhaps like this:
77 | ```clj
78 | (defonce interval-handler ;; notice the use of defonce
79 | (let [live-intervals (atom {})] ;; storage for live intervals
80 | (fn handler [{:keys [action id frequency event]}] ;; the effect handler
81 | (condp = action
82 | :clean (doall ;; <--- new. clean up all existing
83 | (map #(handler {:action :end :id %1}) (keys @live-intervals))
84 | :start (swap! live-intervals assoc id (js/setInterval #(dispatch event) frequency)))
85 | :end (do (js/clearInterval (get @live-intervals id))
86 | (swap! live-intervals dissoc id))))
87 |
88 | ;; when this code is reloaded `:clean` existing intervals
89 | (interval-handler {:action :clean})
90 |
91 | ;; now register
92 | (re-frame.core/reg-fx ;; the re-frame API for registering effect handlers
93 | :interval ;; the effect id
94 | interval-handler)
95 | ```
96 |
97 | **Key takeaway:** every effect handler is statefully grubby in its own special way. So you'll have to
98 | come up with strategies to handle Figwheel reloads on a case by case basis. Sometimes
99 | there's no escaping an application restart.
100 |
101 |
102 | ***
103 |
104 | Up: [FAQ Index](README.md)
105 |
106 |
--------------------------------------------------------------------------------
/docs/FAQs/README.md:
--------------------------------------------------------------------------------
1 | ## Frequently Asked Questions
2 |
3 | 1. [How can I Inspect app-db?](Inspecting-app-db.md)
4 | 2. [How Do I Load On Mount?](LoadOnMount.md) (hint: you don't)
5 | 2. [Reagent looks terrific. Why do I need re-frame?](DoINeedReFrame.md)
6 | 2. [How do I do full-stack re-frame?](FullStackReframe.md)
7 | 2. [How long after I do a dispatch does the event get handled?](When-Does-Dispatch-Happen.md)
8 | 2. [How can I use a subscription in an Event Handler](UseASubscriptionInAnEventHandler.md)
9 | 2. [How do I use logging method X](Logging.md)
10 | 3. [Dispatched Events Are Null](Null-Dispatched-Events.md)
11 | 4. [Why is re-frame implemented in `.cljc` files](Why-CLJC.md)
12 | 5. [Why do we need to clear the subscription cache when reloading with Figwheel?](Why-Clear-Sub-Cache.md)
13 | 6. [How can I detect exceptions in Event Handlers?](CatchingEventExceptions.md)
14 | 7. [How do I store normalised data in app-db?](DB_Normalisation.md)
15 | 8. [How do I register a global interceptor](GlobalInterceptors.md)
16 | 9. [How do I turn on/off polling a database every 60 secs](PollADatabaseEvery60.md)
17 | 10. [re-frame's uses side-effecting registrations. Should I feel dirty?](ViewsOnGlobalRegistration.md)
18 |
19 |
20 | ## Want To Add An FAQ?
21 |
22 | We'd like that. Please supply a PR. Or just open an issue. Many Thanks!!
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/docs/FAQs/UseASubscriptionInAnEventHandler.md:
--------------------------------------------------------------------------------
1 | ### Question
2 |
3 | How do I access the value of a subscription from within an event handler?
4 |
5 | ### The Wrong Way
6 |
7 | You should NOT do this:
8 | ```clj
9 | (re-frame.core/reg-event-db
10 | :event-id
11 | (fn [db v]
12 | (let [sub-val @(subscribe [:something])] ;; <--- Eeek
13 | ....)))
14 | ```
15 |
16 | because that `subscribe`:
17 | 1. might create a memory leak (the subscription might not be "freed")
18 | 2. makes the event handler impure (it grabs a global value)
19 |
20 | ### The Better Way
21 |
22 | Instead, the value of a subscription should
23 | be injected into the `coeffects` of that handler via an interceptor.
24 |
25 | A sketch:
26 | ```clj
27 | (re-frame.core/reg-event-fx ;; handler must access coeffects, so use -fx
28 | :event-id
29 | (inject-sub [:query-id :param]) ;; <-- interceptor will inject subscription value into coeffects
30 | (fn [coeffects event]
31 | (let [sub-val (:something coeffects)] ;; obtain subscription value
32 | ....)))
33 | ```
34 |
35 | Notes:
36 | 1. `inject-sub` is an interceptor which will get the subscription value and add it to coeffects (somehow)
37 | 2. The event handler obtains the value from coeffects
38 |
39 | So, how to write this `inject-sub` interceptor?
40 |
41 | ### Solutions
42 |
43 | re-frame doesn't yet have a builtin `inject-sub` interceptor to do this injection.
44 |
45 | I'd suggest you use this 3rd party library:
46 | https://github.com/vimsical/re-frame-utils/blob/master/src/vimsical/re_frame/cofx/inject.cljc
47 |
48 |
49 | ***
50 |
51 | Up: [FAQ Index](README.md)
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/docs/FAQs/ViewsOnGlobalRegistration.md:
--------------------------------------------------------------------------------
1 |
2 | ### Question
3 |
4 | I feel offended by re-frame's `reg-*` API. How is it functional to side effect globally?
5 |
6 | ### Background
7 |
8 | A re-frame app is defined collectively by its handlers. As an app boots, calls to registration
9 | functions like `reg-event-db` and `reg-sub`
10 | collectively "build up" an app, infusing it with behaviour and capability.
11 |
12 | Currently, this "building up" process involves the progressive mutation of
13 | a global `registrar` (map) held internally within `re-frame`.
14 | Each registration adds a new entry to this `registrar`.
15 |
16 | How should we analyse this from a functional point of view?
17 |
18 | ### Answer
19 |
20 | There are three ways to view this:
21 |
22 | 1. Egads! Say it isn't true. Mutation of a global? Summon the functional lynch mob!
23 |
24 | 2. In theory, top-level side effects will introduce some pain points,
25 | but re-frame's design represents a conscious decision to trade off functional purity
26 | for simplicity of everyday developer experience.
27 | So, yes, re-frame represents a point in
28 | the possible design space, with associated pros and cons. But the cons tend to be
29 | theoretical and the pros are real.
30 |
31 | 3. Actually, there's no purity problem! As a Clojure program
32 | starts, each `defn` (becomes a `def` which) happily
33 | `interns` a symbol and function in [a map-ish structure](https://clojuredocs.org/clojure.core/ns-interns) representing a `namespace`.
34 | The lynch mob stays home for that. The pitchforks remain in their rack.
35 | `re-frame` handler registration
36 | is the same pattern - an `id` and `handler function` are interned
37 | within a map-ish structure (a `registrar`), once, on program load.
38 | So, if you feel uncomfortable with what re-frame does, you should also feel uncomfortable about using `defn`.
39 | Also, it would be useful to understand
40 | [how you are creating a virtual machine when you program re-frame](https://github.com/day8/re-frame/blob/master/docs/MentalModelOmnibus.md#on-dsls-and-machines)
41 |
42 |
43 | While Point 3 is an interesting perspective to consider, the real discussion should probably be around points 1 and 2: is it a good idea for re-frame to tradeoff purity for simplicity? You can't really judge this
44 | properly until you have used it and experienced the simplicity, and/or found pain points (devcards!).
45 | Many people experience few problems and live happily ever after. For others, the conceptual
46 | distaste is insurmountable and nagging. Like it or hate it, please realise it was a deliberate
47 | and conscious design decision, not some oversight.
48 |
49 | --------
50 |
51 | Up: [FAQ Index](README.md)
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | --
60 |
--------------------------------------------------------------------------------
/docs/FAQs/When-Does-Dispatch-Happen.md:
--------------------------------------------------------------------------------
1 | ### Question
2 |
3 | How long after I call `dispatch` will the event be processed?
4 |
5 | ### Answer
6 |
7 | The answer is "it depends", but [this comment in the code](https://github.com/day8/re-frame/blob/master/src/re_frame/router.cljc#L8-L60)
8 | might provide you the answers you seek.
9 |
10 |
11 | Up: [FAQ Index](README.md)
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/docs/FAQs/Why-CLJC.md:
--------------------------------------------------------------------------------
1 | ### Question
2 |
3 | Why is re-frame implemented in `.cljc` files? Aren't ClojureScript
4 | files meant to be `.cljs`?
5 |
6 | ### Answer
7 |
8 | So tests can be run on both the JVM and the JS platforms,
9 | re-frame's implementation is mostly in `.cljc` files.
10 |
11 | The trailing `c` in `.cljc` stands for `common`.
12 |
13 | Necessary interop for each platform can be found in
14 | `interop.clj` (for the JVM) and `interop.cljs` (for JS).
15 |
16 | See also: https://github.com/day8/re-frame-test
17 |
18 |
19 | ***
20 |
21 | Up: [FAQ Index](README.md)
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/docs/FAQs/Why-Clear-Sub-Cache.md:
--------------------------------------------------------------------------------
1 | ### Question
2 |
3 | Why do we call `clear-subscription-cache!` when reloading code with Figwheel?
4 |
5 | ### Answer
6 |
7 | Pour yourself a drink, as this is a circuitous tale involving one of the hardest
8 | problems in Computer Science.
9 |
10 | **1: Humble beginnings**
11 |
12 | When React is rendering, if an exception is thrown, it doesn't catch or
13 | handle the errors gracefully. Instead, all of the React components up to
14 | the root are destroyed. When these components are destroyed, none of their
15 | standard lifecycle methods are called, like `ComponentDidUnmount`.
16 |
17 |
18 | **2: Simple assumptions**
19 |
20 | Reagent tracks the watchers of a Reaction to know when no-one is watching and
21 | it can call the Reaction's `on-dispose`. Part of the book-keeping involved in
22 | this requires running the `on-dispose` in a React `ComponentWillUnmount` lifecycle
23 | method.
24 |
25 | At this point, your spidey senses are probably tingling.
26 |
27 | **3: The hardest problem in CS**
28 |
29 | re-frame subscriptions are created as Reactions. re-frame helpfully deduplicates
30 | subscriptions if multiple parts of the view request the same subscription. This
31 | is a big efficiency boost. When re-frame creates the subscription Reaction, it
32 | sets the `on-dispose` method of that subscription to remove itself from the
33 | subscription cache. This means that when that subscription isn't being watched
34 | by any part of the view, it can be disposed.
35 |
36 | **4: The gnarly implications**
37 |
38 | If you are
39 |
40 | 1. Writing a re-frame app
41 | 2. Write a bug in your subscription code (your one bug for the year)
42 | 3. Which causes an exception to be thrown in your rendering code
43 |
44 | then:
45 |
46 | 1. React will destroy all of the components in your view without calling `ComponentWillUnmount`.
47 | 2. Reagent will not get notified that some subscriptions are not needed anymore.
48 | 3. The subscription on-dispose functions that should have been run, are not.
49 | 4. re-frame's subscription cache will not be invalidated correctly, and the subscription with the bug
50 | is still in the cache.
51 |
52 | At this point you are looking at a blank screen. After debugging, you find the problem and fix it.
53 | You save your code and Figwheel recompiles and reloads the changed code. Figwheel attempts to re-render
54 | from the root. This causes all of the Reagent views to be rendered and to request re-frame subscriptions
55 | if they need them. Because the old buggy subscription is still sitting around in the cache, re-frame
56 | will return that subscription instead of creating a new one based on the fixed code. The only way around
57 | this (once you realise what is going on) is to reload the page.
58 |
59 | **5: Coda**
60 |
61 | re-frame 0.9.0 provides a new function: `re-frame.core/clear-subscription-cache!` which will run the
62 | on-dispose function for every subscription in the cache, emptying the cache, and causing new subscriptions
63 | to be created after reloading.
64 |
65 | ***
66 |
67 | Up: [FAQ Index](README.md)
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/docs/Figma Infographics/inforgraphics.fig:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/Figma Infographics/inforgraphics.fig
--------------------------------------------------------------------------------
/docs/Namespaced-Keywords.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Table Of Contents
4 |
5 | - [Namespaced Ids](#namespaced-ids)
6 |
7 |
8 |
9 | ## Namespaced Ids
10 |
11 | As an app gets bigger, you'll tend to get clashes on ids - event-ids, or query-ids (subscriptions), etc.
12 |
13 | One panel will need to `dispatch` an `:edit` event and so will
14 | another, but the two panels will have different handlers.
15 | So how then to not have a clash? How then to distinguish between
16 | one `:edit` event and another?
17 |
18 | Your goal should be to use event-ids which encode both the event
19 | itself (`:edit` ?) and the context (`:panel1` or `:panel2` ?).
20 |
21 | Luckily, ClojureScript provides a nice easy solution: use keywords
22 | with a __synthetic namespace__. Perhaps something like `:panel1/edit` and `:panel2/edit`.
23 |
24 | You see, ClojureScript allows the namespace in a keyword to be a total
25 | fiction. I can have the keyword `:panel1/edit` even though
26 | `panel1.cljs` doesn't exist.
27 |
28 | Naturally, you'll take advantage of this by using keyword namespaces
29 | which are both unique and descriptive.
30 |
31 | ***
32 |
33 | Previous: [Navigation](Navigation.md)
34 | Up: [Index](README.md)
35 | Next: [Loading Initial Data](Loading-Initial-Data.md)
36 |
--------------------------------------------------------------------------------
/docs/Navigation.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Table Of Contents
4 |
5 | - [What About Navigation?](#what-about-navigation)
6 |
7 |
8 |
9 |
10 | ## What About Navigation?
11 |
12 | How do I switch between different panels of a larger app?
13 |
14 | Your `app-db` could have an `:active-panel` key containing an id for the panel being displayed.
15 |
16 |
17 | When the user does something navigation-ish (selects a tab, a dropdown or something which changes the active panel), then the associated event and dispatch look like this:
18 |
19 | ```clj
20 | (re-frame/reg-event-db
21 | :set-active-panel
22 | (fn [db [_ value]]
23 | (assoc db :active-panel value)))
24 |
25 | (re-frame/dispatch
26 | [:set-active-panel :panel1])
27 | ```
28 |
29 | A high level reagent view has a subscription to :active-panel and will switch to the associated panel.
30 |
31 | ```clj
32 | (re-frame/reg-sub
33 | :active-panel
34 | (fn [db _]
35 | (:active-panel db)))
36 |
37 | (defn panel1
38 | []
39 | [:div {:on-click #(re-frame/dispatch [:set-active-panel :panel2])}
40 | "Here" ])
41 |
42 | (defn panel2
43 | []
44 | [:div "There"])
45 |
46 | (defn high-level-view
47 | []
48 | (let [active (re-frame/subscribe [:active-panel])]
49 | (fn []
50 | [:div
51 | [:div.title "Heading"]
52 | (condp = @active ;; or you could look up in a map
53 | :panel1 [panel1]
54 | :panel2 [panel2])])))
55 | ```
56 |
57 |
58 | Continue to [Namespaced Keywords](Namespaced-Keywords.md) to reduce clashes on ids.
59 |
60 | ***
61 |
62 | Previous: [App Structure](App-Structure.md)
63 | Up: [Index](README.md)
64 | Next: [Namespaced Keywords](Namespaced-Keywords.md)
65 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | ## Introduction
2 | * First, read this repo's [README](../README.md)
3 | * [app-db (Application State)](ApplicationState.md)
4 | * [First Code Walk-Through](CodeWalkthrough.md)
5 | * [Infographic: A re-frame Epoch](AnEpoch.md)
6 |
7 |
8 | ## Dominoes 2 & 3 (event, effect and coeffect handling)
9 | * [Infographic: Event Processing](EventHandlingInfographic.md)
10 | * [Effectful Handlers](EffectfulHandlers.md)
11 | * [Interceptors](Interceptors.md)
12 | * [Effects](Effects.md)
13 | * [Coeffects](Coeffects.md)
14 |
15 | ## Domino 4 (Subscriptions)
16 |
17 | * [Infographic: Subscriptions and The Signal Graph](SubscriptionInfographic.md)
18 | * [Correcting a wrong](SubscriptionsCleanup.md)
19 | * [Flow Mechanics](SubscriptionFlow.md)
20 |
21 | ## Domino 5 (Reagent)
22 |
23 | All the material you need is [here, in Reagent's /doc](https://github.com/reagent-project/reagent/blob/master/doc/README.md).
24 |
25 | ## Deepen/Broaden Your Understanding
26 | * [The API](API.md)
27 | * [An interesting overview of re-frame by purelyfunctional.tv (external link)](https://purelyfunctional.tv/guide/re-frame-building-blocks/)
28 | * [Mental Model Omnibus](MentalModelOmnibus.md)
29 | * [Interesting Resources - including example apps](External-Resources.md)
30 | * [FAQs](FAQs/README.md)
31 |
32 | ## App Structure
33 | * [App Structure](App-Structure.md)
34 | * [On naming things and app-db structure (external link)](https://purelyfunctional.tv/guide/database-structure-in-re-frame/)
35 | * [Navigation](Navigation.md)
36 | * [Namespaced Keywords](Namespaced-Keywords.md)
37 |
38 | ## App Data
39 | * [Loading Initial Data](Loading-Initial-Data.md)
40 | * [Talking To Servers](Talking-To-Servers.md)
41 | * [Subscribing to External Data](Subscribing-To-External-Data.md)
42 |
43 | ## Debugging And Testing
44 | * [Debugging Event Handlers](Debugging-Event-Handlers.md)
45 | * [Debugging](Debugging.md)
46 | * [Testing](Testing.md)
47 |
48 | ## Commercial-Grade Video Training
49 |
50 | * [purelyfunctional.tv](https://purelyfunctional.tv/courses/understanding-re-frame/)
51 | * [Lambda Island Videos](https://lambdaisland.com/collections/react-reagent-re-frame)
52 |
53 | ## Miscellaneous
54 | * [Eek! Performance Problems](Performance-Problems.md)
55 | * [Solve the CPU hog problem](Solve-the-CPU-hog-problem.md)
56 | * [Using Stateful JS Components](Using-Stateful-JS-Components.md)
57 | * [The re-frame Logo](The-re-frame-logo.md)
58 |
--------------------------------------------------------------------------------
/docs/SubscriptionInfographic.md:
--------------------------------------------------------------------------------
1 | ## Subscriptions
2 |
3 | A re-frame app is 75% derived data.
4 |
5 | `app-db` is the root, authoritative source of data but radiating
6 | from it is a graph of nodes all computing derived data. Ultimately, the leaf nodes of
7 | this graph are ViewFunctions which compute hiccup (yes, derived data)
8 | which is rendered into the application's UI. But sitting within
9 | the graph, between the root and these leaves, are intermediate
10 | computational nodes supplied by subscriptions which compute
11 | materialised views of the data in `app-db`.
12 |
13 | ## The Domino Narrative
14 |
15 | In terms of the dominos narrative, subscriptions are domino 4,
16 | and the leaf View Functions are domino 5. For tutorial purposes,
17 | we distinguish between them - they serve different purposes - but
18 | they are, conceptually, all nodes in the same graph.
19 |
20 | ## Graph Shapes
21 |
22 | In the simplest version of a graph, subscriptions simply extract
23 | some part of the data in `app-db`, which then flows on into
24 | ViewFunctions unchanged.
25 |
26 | In more complex examples, subscriptions are
27 | layered, with some obtaining data from one or more other
28 | subscriptions, before a ViewFunctions eventually receive
29 | highly processed versions of what's in `app-db`.
30 |
31 | ## The Layers
32 |
33 | The layers in this graph are as follows:
34 | - layer 1 is the root node, `app-db`.
35 | - layer 2 subscriptions extract data directly from `app-db`.
36 | - layer 3 subscriptions obtain data from other subscriptions (not `app-db`), and compute derived data.
37 | - layer 4 the view functions which compute hiccup (more derived data)
38 |
39 | As we'll see soon, there's efficency reasons to distinguish between layer 2 (extractor)
40 | and layer 3 (materialised view).
41 |
42 | ## reg-sub
43 |
44 | Subscription handlers are registered using `reg-sub`.
45 |
46 | But note: just because you register a handler doesn't mean that node exists in
47 | the graph. You are only defining how the node would compute if it was needed.
48 |
49 | Nodes in the signal graph are created and destroyed according to the demands
50 | of (leaf) ViewFunction nodes (layer 4).
51 |
52 | When a ViewFunction (layer 4) uses a subscription, the graph of nodes needed to service
53 | that subscription will be created and, later, when the ViewFunction is destroyed
54 | that part of the graph will also be destroyed (unless used for other purposes).
55 |
56 | ## An Infographic Depiction
57 |
58 | Please read the following infographic carefully
59 | because it contains important notes.
60 |
61 |
62 |
63 | ## Example Use
64 |
65 | To see `reg-sub` used in a real application, please read through the
66 | heavily commented subscription code
67 | [in the todomvc example](https://github.com/day8/re-frame/blob/master/examples/todomvc/src/todomvc/subs.cljs).
68 |
69 |
70 |
71 | ***
72 |
73 | Previous: [Coeffects](Coeffects.md)
74 | Up: [Index](README.md)
75 | Next: [Correcting a wrong](SubscriptionsCleanup.md)
76 |
77 |
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/docs/Talking-To-Servers.md:
--------------------------------------------------------------------------------
1 |
2 | ## Talking To Servers
3 |
4 | This page describes how a re-frame app might "talk" to a backend HTTP server.
5 |
6 | We'll assume there's a json-returning server endpoint
7 | at "http://json.my-endpoint.com/blah". We want to GET from that
8 | endpoint and put a processed version of the returned json into `app-db`.
9 |
10 | ## Triggering The Request
11 |
12 | The user often does something to trigger the process.
13 |
14 | Here's a button which the user could click:
15 | ```clj
16 | (defn request-it-button
17 | []
18 | [:div {:class "button-class"
19 | :on-click #(dispatch [:request-it])} ;; get data from the server !!
20 | "I want it, now!"])
21 | ```
22 |
23 | Notice the `on-click` handler - it `dispatch`es the event `[:request-it]`.
24 |
25 | ## The Event Handler
26 |
27 | That `:request-it` event will need to be "handled", which means an event handler must be registered for it.
28 |
29 | We want this handler to:
30 | 1. Initiate the HTTP GET
31 | 2. Update a flag in `app-db` which will trigger a modal "Loading ..." message for the user to see
32 |
33 | We're going to create two versions of this event handler. First, we'll create a
34 | problematic version of the event handler and then, realising our sins, we'll write
35 | a second version which is a soaring paragon of virtue. Both versions
36 | will teach us something.
37 |
38 |
39 | ### Version 1
40 |
41 | We're going to use the [cljs-ajax library](https://github.com/JulianBirch/cljs-ajax) as the HTTP workhorse.
42 |
43 | Here's the event handler:
44 | ```clj
45 | (ns my.app.events ;; <1>
46 | (:require [ajax.core :refer [GET]]
47 | [re-frame.core :refer [reg-event-db]))
48 |
49 | (reg-event-db ;; <-- register an event handler
50 | :request-it ;; <-- the event id
51 | (fn ;; <-- the handler function
52 | [db _]
53 |
54 | ;; kick off the GET, making sure to supply a callback for success and failure
55 | (GET
56 | "http://json.my-endpoint.com/blah"
57 | {:handler #(dispatch [:process-response %1]) ;; <2> further dispatch !!
58 | :error-handler #(dispatch [:bad-response %1])}) ;; <2> further dispatch !!
59 |
60 | ;; update a flag in `app-db` ... presumably to cause a "Loading..." UI
61 | (assoc db :loading? true))) ;; <3> return an updated db
62 | ```
63 |
64 | Further Notes:
65 | 1. Event handlers are normally put into an `events.cljs` namespace
66 | 2. Notice that the GET callbacks issue a further `dispatch`. Such callbacks
67 | should never attempt to close over `db` themselves, or make
68 | any changes to it because, by the time these callbacks happen, the value
69 | in `app-db` may have changed. Whereas, if they `dispatch`, then the event
70 | handlers looking after the event they dispatch will be given the latest copy of the db.
71 | 3. event handlers registered using `reg-event-db` must return a new value for
72 | `app-db`. In our case, we set a flag which will presumably cause a "Loading ..."
73 | UI to show.
74 |
75 | ### Successful GET
76 |
77 | As we noted above, the on-success handler itself is just
78 | `#(dispatch [:process-response RESPONSE])`. So we'll need to register a handler
79 | for this event too.
80 |
81 | Like this:
82 | ```clj
83 | (reg-event-db
84 | :process-response
85 | (fn
86 | [db [_ response]] ;; destructure the response from the event vector
87 | (-> db
88 | (assoc :loading? false) ;; take away that "Loading ..." UI
89 | (assoc :data (js->clj response)))) ;; fairly lame processing
90 | ```
91 |
92 | A normal handler would have more complex processing of the response. But we're
93 | just sketching here, so we've left it easy.
94 |
95 | There'd also need to be a handler for the `:bad-response` event too. Left as an exercise.
96 |
97 | ### Problems In Paradise?
98 |
99 | This approach will work, and it is useful to take time to understand why it
100 | would work, but it has a problem: the event handler isn't pure.
101 |
102 | That `GET` is a side effect, and side effecting functions are like a
103 | well salted paper cut. We try hard to avoid them.
104 |
105 | ### Version 2
106 |
107 | The better solution is, of course, to use an effectful handler. This
108 | is explained in detail in the previous tutorials: [Effectful Handlers](EffectfulHandlers.md)
109 | and [Effects](Effects.md).
110 |
111 | In the 2nd version, we use the alternative registration function, `reg-event-fx` , and we'll use an
112 | "Effect Handler" supplied by this library
113 | [https://github.com/day8/re-frame-http-fx](https://github.com/day8/re-frame-http-fx).
114 | You may soon feel confident enough to write your own.
115 |
116 | Here's our rewrite:
117 |
118 | ```clj
119 | (ns my.app.events
120 | (:require
121 | [ajax.core :as ajax]
122 | [day8.re-frame.http-fx]
123 | [re-frame.core :refer [reg-event-fx]))
124 |
125 | (reg-event-fx ;; <-- note the `-fx` extension
126 | :request-it ;; <-- the event id
127 | (fn ;; <-- the handler function
128 | [{db :db} _] ;; <-- 1st argument is coeffect, from which we extract db
129 |
130 | ;; we return a map of (side) effects
131 | {:http-xhrio {:method :get
132 | :uri "http://json.my-endpoint.com/blah"
133 | :format (ajax/json-request-format)
134 | :response-format (ajax/json-response-format {:keywords? true})
135 | :on-success [:process-response]
136 | :on-failure [:bad-response]}
137 | :db (assoc db :loading? true)}))
138 | ```
139 |
140 | Notes:
141 | 1. Our event handler "describes" side effects, it does not "do" side effects
142 | 2. The event handler we wrote for `:process-response` stays as it was
143 |
144 |
145 |
146 | ***
147 |
148 | Previous: [Loading Initial Data](Loading-Initial-Data.md)
149 | Up: [Index](README.md)
150 | Next: [Subscribing to External Data](Subscribing-To-External-Data.md)
151 |
152 |
153 |
154 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/docs/The-re-frame-logo.md:
--------------------------------------------------------------------------------
1 | ## The re-frame Logo
2 |
3 | 
4 |
5 | ### Who
6 |
7 | Created by the mysterious, deep thinker, known only as @martinklepsch.
8 |
9 | Some say he appears on high value stamps in Germany and that he once
10 | punched a horse to the ground. Others say he loves recursion so much that,
11 | in his wallet, he carries a photograph of his wallet.
12 |
13 | All we know for sure is that he wields [Sketch.app](https://www.sketchapp.com/) like Bruce Lee
14 | wielded nunchucks.
15 |
16 | ### Genesis Theories
17 |
18 | Great, unexplained works encourage fan theories, and the re-frame
19 | logo is no exception.
20 |
21 | One noisy group insists that @martinklepsch's design is assertively
22 | trying to `Put the 'f' back into infinity`. They have t-shirts.
23 |
24 | Another group speculates that he created the logo as a bifarious
25 | rainbow homage to Frank Lloyd Wright's masterpiece, the Guggenheim
26 | Museum. Which is surely a classic case of premature abstraction
27 | and over engineering. Their theory, not the Guggenheim.
28 |
29 | 
30 |
31 | The infamous "Bad Touch" faction look at the logo and see the cljs
32 | logo mating noisily with re-frame's official architecture diagram.
33 | Yes, its true, their parties are completely awesome, but you will
34 | need someone to bail you out of jail later.
35 |
36 | 
37 |
38 | For the Functional Fundamentalists, a stern bunch, the logo is a
39 | flowing poststructuralist rebuttal of OO's vowel duplication and
40 | horizontal adjacency. Their alternative approach, FF, is fine, apparently,
41 | because "everyone loves a fricative".
42 |
43 | For his part, @martinklepsch has never confirmed any theory, teasing
44 | us instead with coded clues like "Will you please stop emailing me"
45 | and "Why did you say I hit a horse?".
46 |
47 | ### Assets Where?
48 |
49 | Within this repo, look in `/images/logo/`
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/docs/Using-Stateful-JS-Components.md:
--------------------------------------------------------------------------------
1 | ## Using Stateful JS Components
2 |
3 | You know what's good for you, and you know what's right. But it
4 | doesn't matter - the wickedness of the temptation is too much.
5 |
6 | The JS world is brimming with shiny component baubles: D3,
7 | Google Maps, Chosen, etc.
8 |
9 | But they are salaciously stateful and mutative. And, you,
10 | raised in a pure, functional home, with caring, immutable parents,
11 | know they are wrong. But, my, how you still yearn for the sweet
12 | thrill of that forbidden fruit.
13 |
14 | I won't tell, if you don't. But careful plans must be made ...
15 |
16 | ### The overall plan
17 |
18 | To use a stateful js component, you'll need to write two Reagent components:
19 | - an **outer component** responsible for sourcing data via a subscription or r/atom or cursor, etc.
20 | - an **inner component** responsible for wrapping and manipulating the stateful JS component via lifecycle functions.
21 |
22 | The pattern involves the outer component, which sources data, supplying this data to the inner component **via props**.
23 |
24 | ### Example Using Google Maps
25 |
26 | ```clj
27 | (defn gmap-inner []
28 | (let [gmap (atom nil)
29 | options (clj->js {"zoom" 9})
30 | update (fn [comp]
31 | (let [{:keys [latitude longitude]} (reagent/props comp)
32 | latlng (js/google.maps.LatLng. latitude longitude)]
33 | (.setPosition (:marker @gmap) latlng)
34 | (.panTo (:map @gmap) latlng)))]
35 |
36 | (reagent/create-class
37 | {:reagent-render (fn []
38 | [:div
39 | [:h4 "Map"]
40 | [:div#map-canvas {:style {:height "400px"}}]])
41 |
42 | :component-did-mount (fn [comp]
43 | (let [canvas (.getElementById js/document "map-canvas")
44 | gm (js/google.maps.Map. canvas options)
45 | marker (js/google.maps.Marker. (clj->js {:map gm :title "Drone"}))]
46 | (reset! gmap {:map gm :marker marker}))
47 | (update comp))
48 |
49 | :component-did-update update
50 | :display-name "gmap-inner"})))
51 |
52 |
53 |
54 | (defn gmap-outer []
55 | (let [pos (subscribe [:current-position])] ;; obtain the data
56 | (fn []
57 | ;; Note: @pos is a map here, so it gets passed as props.
58 | ;; Non-props values can be accessed via (reagent/argv comp)
59 | [gmap-inner @pos])))
60 | ```
61 |
62 |
63 | Notes:
64 | - `gmap-outer` obtains data via a subscription. It is quite simple - trivial almost.
65 | - it then passes this data __as a prop__ to `gmap-inner`. This inner component has the job of wrapping/managing the stateful js component (Gmap in our case above)
66 | - when the data (delivered by the subscription) to the outer layer changes, the inner layer, `gmap-inner`, will be given a new prop - `@pos` in the case above.
67 | - when the inner component is given new props, its entire set of lifecycle functions will be engaged.
68 | - the renderer for the inner layer ALWAYS renders the same, minimal container hiccup for the component. Even though the `props` have changed, the same hiccup is output. So it will appear to React as if nothing changes from one render to the next. No work to be done. React/Reagent will leave the DOM untouched.
69 | - but this inner component has other lifecycle functions and this is where the real work is done.
70 | - for example, after the renderer is called (which ignores its props), `component-did-update` will be called. In this function, we don't ignore the props, and we use them to update/mutate the stateful JS component.
71 | - the props passed (in this case `@pos`) in must be a map, otherwise `(reagent/props comp)` will return nil.
72 |
73 | ### Pattern Discovery
74 |
75 | This pattern has been independently discovered by many. To my knowledge,
76 | [this description of the Container/Component pattern](https://medium.com/@learnreact/container-components-c0e67432e005#.3ic1uipvu)
77 | is the first time it was written up.
78 |
79 | ### Code Credit
80 |
81 | The example gmaps code above was developed by @jhchabran in this gist:
82 | https://gist.github.com/jhchabran/e09883c3bc1b703a224d#file-2_google_map-cljs
83 |
84 | ### D3 Examples
85 |
86 | D3 (from @zachcp):
87 | - Blog Post: http://zachcp.org/blog/2015/reagent-d3/
88 | - Code: https://github.com/zachcp/simplecomponent
89 | - Example: http://zachcp.github.io/simplecomponent/
90 |
91 | RID3, a reagent interface to D3
92 | - Repo: https://github.com/gadfly361/rid3
93 | - Demo: https://rawgit.com/gadfly361/rid3/master/dev-resources/public/examples.html
94 |
95 | ### JS Interop
96 |
97 | You'll probably need to know how to do interop with js:
98 | http://www.spacjer.com/blog/2014/09/12/clojurescript-javascript-interop/
99 |
100 | Perhaps use this library to make it even easier:
101 | https://github.com/binaryage/cljs-oops
102 |
103 | ### Advanced Lifecycle Methods
104 |
105 | If you mess around with lifecycle methods, you'll probably want to read Martin's explanations:
106 | https://www.martinklepsch.org/posts/props-children-and-component-lifecycle-in-reagent.html
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/docs/images/Readme/6dominoes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/Readme/6dominoes.png
--------------------------------------------------------------------------------
/docs/images/Readme/Dominoes-small.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/Readme/Dominoes-small.jpg
--------------------------------------------------------------------------------
/docs/images/Readme/Dominoes.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/Readme/Dominoes.jpg
--------------------------------------------------------------------------------
/docs/images/Readme/todolist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/Readme/todolist.png
--------------------------------------------------------------------------------
/docs/images/epoch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/epoch.png
--------------------------------------------------------------------------------
/docs/images/event-handlers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/event-handlers.png
--------------------------------------------------------------------------------
/docs/images/example_app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/example_app.png
--------------------------------------------------------------------------------
/docs/images/logo/Genesis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/logo/Genesis.png
--------------------------------------------------------------------------------
/docs/images/logo/Guggenheim.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/logo/Guggenheim.jpg
--------------------------------------------------------------------------------
/docs/images/logo/README.md:
--------------------------------------------------------------------------------
1 |
2 | 
3 |
4 | [Read the backstory here.](/docs/The-re-frame-logo.md)
5 |
6 | Created via [Sketch.app](https://www.sketchapp.com/). See the file `re-frame-logo.sketch`
7 |
8 | Unfortunately the gradients are not exported properly so we can't provide an SVG here for now.
9 |
--------------------------------------------------------------------------------
/docs/images/logo/frame_1024w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/logo/frame_1024w.png
--------------------------------------------------------------------------------
/docs/images/logo/re-frame-logo.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/logo/re-frame-logo.sketch
--------------------------------------------------------------------------------
/docs/images/logo/re-frame_128w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/logo/re-frame_128w.png
--------------------------------------------------------------------------------
/docs/images/logo/re-frame_256w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/logo/re-frame_256w.png
--------------------------------------------------------------------------------
/docs/images/logo/re-frame_512w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/logo/re-frame_512w.png
--------------------------------------------------------------------------------
/docs/images/mental-model-omnibus.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/mental-model-omnibus.jpg
--------------------------------------------------------------------------------
/docs/images/scale-changes-everything.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/scale-changes-everything.jpg
--------------------------------------------------------------------------------
/docs/images/subscriptions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/subscriptions.png
--------------------------------------------------------------------------------
/docs/images/the-water-cycle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/the-water-cycle.png
--------------------------------------------------------------------------------
/docs/styles/website.css:
--------------------------------------------------------------------------------
1 | /* .hljs-symbol { */
2 | /* color: #AD81FF !important; */
3 | /* } */
4 |
5 | /* .hljs-name { */
6 | /* color: #67D8EE !important; */
7 | /* } */
8 |
9 | /* .hljs-builtin-name { */
10 | /* color: #A6E131; */
11 | /* } */
12 | .markdown-section > pre {
13 | color: #657b83 !important;
14 | background: #fdf6e3 !important;
15 | }
16 | .hljs {
17 | display: block;
18 | overflow-x: auto;
19 | padding: 0.5em;
20 | background: #fdf6e3;
21 | color: #657b83;
22 | }
23 |
24 | .hljs-comment,
25 | .hljs-quote {
26 | color: #93a1a1;
27 | }
28 |
29 | /* Solarized Green */
30 | .hljs-keyword,
31 | .hljs-selector-tag,
32 | .hljs-addition {
33 | color: #859900;
34 | }
35 |
36 | /* Solarized Cyan */
37 | .hljs-number,
38 | .hljs-string,
39 | .hljs-meta .hljs-meta-string,
40 | .hljs-literal,
41 | .hljs-doctag,
42 | .hljs-regexp {
43 | color: #2aa198;
44 | }
45 |
46 | /* Solarized Blue */
47 | .hljs-title,
48 | .hljs-section,
49 | .hljs-name,
50 | .hljs-selector-id,
51 | .hljs-selector-class {
52 | color: #268bd2;
53 | }
54 |
55 | /* Solarized Yellow */
56 | .hljs-attribute,
57 | .hljs-attr,
58 | .hljs-variable,
59 | .hljs-template-variable,
60 | .hljs-class .hljs-title,
61 | .hljs-type {
62 | color: #b58900;
63 | }
64 |
65 | /* Solarized Orange */
66 | .hljs-symbol,
67 | .hljs-bullet,
68 | .hljs-subst,
69 | .hljs-meta,
70 | .hljs-meta .hljs-keyword,
71 | .hljs-selector-attr,
72 | .hljs-selector-pseudo,
73 | .hljs-link {
74 | color: #cb4b16;
75 | }
76 |
77 | /* Solarized Red */
78 | .hljs-built_in,
79 | .hljs-deletion {
80 | color: #dc322f;
81 | }
82 |
83 | .hljs-formula {
84 | background: #eee8d5;
85 | }
86 |
87 | .hljs-emphasis {
88 | font-style: italic;
89 | }
90 |
91 | .hljs-strong {
92 | font-weight: bold;
93 | }
94 |
--------------------------------------------------------------------------------
/examples/simple/.gitignore:
--------------------------------------------------------------------------------
1 | .shadow-cljs/
2 | package-lock.json
3 | package.json
4 | shadow-cljs.edn
5 |
--------------------------------------------------------------------------------
/examples/simple/README.md:
--------------------------------------------------------------------------------
1 | # A Simple App
2 |
3 | This tiny application is meant to provide a quick start of the basics of re-frame.
4 |
5 | A detailed source code walk-through is provided in the docs:
6 | https://github.com/day8/re-frame/blob/master/docs/CodeWalkthrough.md
7 |
8 | All the code is in one namespace: `/src/simple/core.cljs`.
9 |
10 | ### Run It And Change It
11 |
12 | Steps:
13 |
14 | 1. Check out the re-frame repo
15 | 2. Get a command line
16 | 3. `cd` to the root of this sub project (where this README exists)
17 | 4. Run "`lein do clean, shadow watch client`" to compile the app and start up shadow-cljs hot-reloading
18 | 5. Wait for the compile to finish. At a minumum, that might take 15 seconds. But it can take more like 60 seconds if you are new to ClojureScript and various caches are empty. Eventually you should see `[:client] Build Completed (... stats ...)`
19 | 6. Open `http://localhost:8280/example.html` to see the app
20 |
21 | While step 4 is running, any changes you make to the ClojureScript
22 | source files (in `src`) will be re-compiled and reflected in the running
23 | page immediately.
24 |
25 | ### Production Version
26 |
27 | Run "`lein do clean, shadow release client`" to compile an optimised
28 | version, and then open `resources/public/example.html` in a browser.
29 |
--------------------------------------------------------------------------------
/examples/simple/project.clj:
--------------------------------------------------------------------------------
1 | (defproject simple "lein-git-inject/version"
2 |
3 | :dependencies [[org.clojure/clojure "1.10.1"]
4 | [org.clojure/clojurescript "1.10.597"
5 | :exclusions [com.google.javascript/closure-compiler-unshaded
6 | org.clojure/google-closure-library
7 | org.clojure/google-closure-library-third-party]]
8 | [thheller/shadow-cljs "2.8.83"]
9 | [reagent "0.9.1"]
10 | [re-frame "RELEASE"]]
11 |
12 | :plugins [[day8/lein-git-inject "0.0.11"]
13 | [lein-shadow "0.1.7"]]
14 |
15 | :middleware [leiningen.git-inject/middleware]
16 |
17 | :clean-targets ^{:protect false} [:target-path
18 | "shadow-cljs.edn"
19 | "package.json"
20 | "package-lock.json"
21 | "resources/public/js"]
22 |
23 | :shadow-cljs {:nrepl {:port 8777}
24 |
25 | :builds {:client {:target :browser
26 | :output-dir "resources/public/js"
27 | :modules {:client {:init-fn simple.core/run}}
28 | :devtools {:http-root "resources/public"
29 | :http-port 8280}}}}
30 |
31 | :aliases {"dev-auto" ["shadow" "watch" "client"]})
32 |
--------------------------------------------------------------------------------
/examples/simple/resources/public/example.css:
--------------------------------------------------------------------------------
1 |
2 | div, h1, input {
3 | font-family: HelveticaNeue, Helvetica;
4 | color: #777;
5 | }
6 |
7 | .example-clock {
8 | font-size: 128px;
9 | line-height: 1.2em;
10 | font-family: HelveticaNeue-UltraLight, Helvetica;
11 | }
12 |
13 | @media (max-width: 768px) {
14 | .example-clock {
15 | font-size: 64px;
16 | }
17 | }
18 |
19 | .color-input, .color-input input {
20 | font-size: 24px;
21 | line-height: 1.5em;
22 | }
23 |
--------------------------------------------------------------------------------
/examples/simple/resources/public/example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Example
6 |
7 |
8 |
9 |
10 |
Reframe simple example app – see README.md
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/examples/simple/src/deps.cljs:
--------------------------------------------------------------------------------
1 | {:npm-dev-deps {"shadow-cljs" "2.8.83"}}
2 |
--------------------------------------------------------------------------------
/examples/simple/src/simple/core.cljs:
--------------------------------------------------------------------------------
1 | (ns simple.core
2 | (:require [reagent.core :as reagent]
3 | [re-frame.core :as rf]
4 | [clojure.string :as str]))
5 |
6 | ;; A detailed walk-through of this source code is provided in the docs:
7 | ;; https://github.com/day8/re-frame/blob/master/docs/CodeWalkthrough.md
8 |
9 | ;; -- Domino 1 - Event Dispatch -----------------------------------------------
10 |
11 | (defn dispatch-timer-event
12 | []
13 | (let [now (js/Date.)]
14 | (rf/dispatch [:timer now]))) ;; <-- dispatch used
15 |
16 | ;; Call the dispatching function every second.
17 | ;; `defonce` is like `def` but it ensures only one instance is ever
18 | ;; created in the face of figwheel hot-reloading of this file.
19 | (defonce do-timer (js/setInterval dispatch-timer-event 1000))
20 |
21 |
22 | ;; -- Domino 2 - Event Handlers -----------------------------------------------
23 |
24 | (rf/reg-event-db ;; sets up initial application state
25 | :initialize ;; usage: (dispatch [:initialize])
26 | (fn [_ _] ;; the two parameters are not important here, so use _
27 | {:time (js/Date.) ;; What it returns becomes the new application state
28 | :time-color "#f88"})) ;; so the application state will initially be a map with two keys
29 |
30 |
31 | (rf/reg-event-db ;; usage: (dispatch [:time-color-change 34562])
32 | :time-color-change ;; dispatched when the user enters a new colour into the UI text field
33 | (fn [db [_ new-color-value]] ;; -db event handlers given 2 parameters: current application state and event (a vector)
34 | (assoc db :time-color new-color-value))) ;; compute and return the new application state
35 |
36 |
37 | (rf/reg-event-db ;; usage: (dispatch [:timer a-js-Date])
38 | :timer ;; every second an event of this kind will be dispatched
39 | (fn [db [_ new-time]] ;; note how the 2nd parameter is destructured to obtain the data value
40 | (assoc db :time new-time))) ;; compute and return the new application state
41 |
42 |
43 | ;; -- Domino 4 - Query -------------------------------------------------------
44 |
45 | (rf/reg-sub
46 | :time
47 | (fn [db _] ;; db is current app state. 2nd unused param is query vector
48 | (:time db))) ;; return a query computation over the application state
49 |
50 | (rf/reg-sub
51 | :time-color
52 | (fn [db _]
53 | (:time-color db)))
54 |
55 |
56 | ;; -- Domino 5 - View Functions ----------------------------------------------
57 |
58 | (defn clock
59 | []
60 | [:div.example-clock
61 | {:style {:color @(rf/subscribe [:time-color])}}
62 | (-> @(rf/subscribe [:time])
63 | .toTimeString
64 | (str/split " ")
65 | first)])
66 |
67 | (defn color-input
68 | []
69 | [:div.color-input
70 | "Time color: "
71 | [:input {:type "text"
72 | :value @(rf/subscribe [:time-color])
73 | :on-change #(rf/dispatch [:time-color-change (-> % .-target .-value)])}]]) ;; <---
74 |
75 | (defn ui
76 | []
77 | [:div
78 | [:h1 "Hello world, it is now"]
79 | [clock]
80 | [color-input]])
81 |
82 | ;; -- Entry Point -------------------------------------------------------------
83 |
84 | (defn render
85 | []
86 | (reagent/render [ui]
87 | (js/document.getElementById "app")))
88 |
89 | (defn ^:dev/after-load clear-cache-and-render!
90 | []
91 | ;; The `:dev/after-load` metadata causes this function to be called
92 | ;; after shadow-cljs hot-reloads code. We force a UI update by clearing
93 | ;; the Reframe subscription cache.
94 | (rf/clear-subscription-cache!)
95 | (render))
96 |
97 | (defn run
98 | []
99 | (rf/dispatch-sync [:initialize]) ;; put a value into application state
100 | (render) ;; mount the application's ui into ''
101 | )
102 |
--------------------------------------------------------------------------------
/examples/todomvc/.gitignore:
--------------------------------------------------------------------------------
1 | resources/public/js
2 | .shadow-cljs/
3 | package-lock.json
4 | package.json
5 | shadow-cljs.edn
6 |
--------------------------------------------------------------------------------
/examples/todomvc/README.md:
--------------------------------------------------------------------------------
1 | # TodoMVC done with re-frame
2 |
3 | A [re-frame](https://github.com/day8/re-frame) implementation of [TodoMVC](http://todomvc.com/).
4 |
5 | But this is NOT your normal, lean and minimal todomvc implementation,
6 | geared towards showing how easily re-frame can implement the challenge.
7 |
8 | Instead, this todomvc example has evolved into more of a teaching tool
9 | and we've thrown in more advanced re-frame features which are not
10 | really required to get the job done. So lean and minimal is no longer a goal.
11 |
12 |
13 | ## Setup And Run
14 |
15 | 1. Install [Leiningen](http://leiningen.org/) (plus Java).
16 |
17 | 2. Get the re-frame repo
18 | ```
19 | git clone https://github.com/day8/re-frame.git
20 | ```
21 |
22 | 3. cd to the right example directory
23 | ```
24 | cd re-frame/examples/todomvc
25 | ```
26 |
27 | 4. Clean build
28 | ```
29 | lein do clean, shadow watch client
30 | ```
31 |
32 | 5. Wait for step 4 to do the compile, and then run the built app:
33 | ```
34 | open http://localhost:8280
35 | ```
36 |
37 |
38 | ## Compile an optimised version
39 |
40 | 1. Compile
41 | ```
42 | lein do clean, shadow release client
43 | ```
44 |
45 | 2. Open the following in your browser
46 | ```
47 | resources/public/index.html
48 | ```
49 |
50 |
51 | ## Exploring The Code
52 |
53 | From the re-frame readme:
54 | ```
55 | To build a re-frame app, you:
56 | - design your app's data structure (data layer)
57 | - write and register subscription functions (query layer)
58 | - write Reagent component functions (view layer)
59 | - write and register event handler functions (control layer and/or state transition layer)
60 | ```
61 |
62 | In `src`, there's a matching set of files (each small):
63 | ```
64 | src
65 | ├── core.cljs <--- entry point, plus history
66 | ├── db.cljs <--- data related (data layer)
67 | ├── subs.cljs <--- subscription handlers (query layer)
68 | ├── views.cljs <--- reagent components (view layer)
69 | └── events.cljs <--- event handlers (control/update layer)
70 | ```
71 |
72 | ## Further Notes
73 |
74 | The [official reagent example](https://github.com/reagent-project/reagent/tree/master/examples/todomvc).
75 |
--------------------------------------------------------------------------------
/examples/todomvc/project.clj:
--------------------------------------------------------------------------------
1 | (defproject todomvc-re-frame "lein-git-inject/version"
2 |
3 | :dependencies [[org.clojure/clojure "1.10.1"]
4 | [org.clojure/clojurescript "1.10.597"
5 | :exclusions [com.google.javascript/closure-compiler-unshaded
6 | org.clojure/google-closure-library
7 | org.clojure/google-closure-library-third-party]]
8 | [thheller/shadow-cljs "2.8.83"]
9 | [reagent "0.9.1"]
10 | [re-frame "RELEASE"]
11 | [binaryage/devtools "0.9.10"]
12 | [clj-commons/secretary "1.2.4"]
13 | [day8.re-frame/tracing "0.5.3"]]
14 |
15 | :plugins [[day8/lein-git-inject "0.0.11"]
16 | [lein-shadow "0.1.7"]]
17 |
18 | :middleware [leiningen.git-inject/middleware]
19 |
20 | :clean-targets ^{:protect false} [:target-path
21 | "shadow-cljs.edn"
22 | "package.json"
23 | "package-lock.json"
24 | "resources/public/js"]
25 |
26 | :shadow-cljs {:nrepl {:port 8777}
27 |
28 | :builds {:client {:target :browser
29 | :output-dir "resources/public/js"
30 | :modules {:client {:init-fn todomvc.core/main}}
31 | :devtools {:http-root "resources/public"
32 | :http-port 8280}}}}
33 |
34 | :aliases {"dev-auto" ["shadow" "watch" "client"]})
35 |
--------------------------------------------------------------------------------
/examples/todomvc/resources/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Reframe Todomvc
6 |
7 |
8 |
9 |
10 |
Reframe Todomvc example app – see README.md
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/examples/todomvc/src/deps.cljs:
--------------------------------------------------------------------------------
1 | {:npm-dev-deps {"shadow-cljs" "2.8.83"}}
2 |
--------------------------------------------------------------------------------
/examples/todomvc/src/todomvc/core.cljs:
--------------------------------------------------------------------------------
1 | (ns todomvc.core
2 | (:require-macros [secretary.core :refer [defroute]])
3 | (:require [goog.events :as events]
4 | [reagent.core :as reagent]
5 | [re-frame.core :as rf :refer [dispatch dispatch-sync]]
6 | [secretary.core :as secretary]
7 | [todomvc.events] ;; These two are only required to make the compiler
8 | [todomvc.subs] ;; load them (see docs/App-Structure.md)
9 | [todomvc.views]
10 | [devtools.core :as devtools])
11 | (:import [goog History]
12 | [goog.history EventType]))
13 |
14 |
15 | ;; -- Debugging aids ----------------------------------------------------------
16 | (devtools/install!) ;; we love https://github.com/binaryage/cljs-devtools
17 | (enable-console-print!) ;; so that println writes to `console.log`
18 |
19 |
20 | ;; Put an initial value into app-db.
21 | ;; The event handler for `:initialise-db` can be found in `events.cljs`
22 | ;; Using the sync version of dispatch means that value is in
23 | ;; place before we go onto the next step.
24 | (dispatch-sync [:initialise-db])
25 |
26 | ;; -- Routes and History ------------------------------------------------------
27 | ;; Although we use the secretary library below, that's mostly a historical
28 | ;; accident. You might also consider using:
29 | ;; - https://github.com/DomKM/silk
30 | ;; - https://github.com/juxt/bidi
31 | ;; We don't have a strong opinion.
32 | ;;
33 | (defroute "/" [] (dispatch [:set-showing :all]))
34 | (defroute "/:filter" [filter] (dispatch [:set-showing (keyword filter)]))
35 |
36 | (defonce history
37 | (doto (History.)
38 | (events/listen EventType.NAVIGATE
39 | (fn [event] (secretary/dispatch! (.-token event))))
40 | (.setEnabled true)))
41 |
42 |
43 | ;; -- Entry Point -------------------------------------------------------------
44 |
45 | (defn render
46 | []
47 | ;; Render the UI into the HTML's element
48 | ;; The view function `todomvc.views/todo-app` is the
49 | ;; root view for the entire UI.
50 | (reagent/render [todomvc.views/todo-app]
51 | (.getElementById js/document "app")))
52 |
53 | (defn ^:dev/after-load clear-cache-and-render!
54 | []
55 | ;; The `:dev/after-load` metadata causes this function to be called
56 | ;; after shadow-cljs hot-reloads code. We force a UI update by clearing
57 | ;; the Reframe subscription cache.
58 | (rf/clear-subscription-cache!)
59 | (render))
60 |
61 | (defn ^:export main
62 | []
63 | (render))
64 |
--------------------------------------------------------------------------------
/examples/todomvc/src/todomvc/db.cljs:
--------------------------------------------------------------------------------
1 | (ns todomvc.db
2 | (:require [cljs.reader]
3 | [cljs.spec.alpha :as s]
4 | [re-frame.core :as re-frame]))
5 |
6 |
7 | ;; -- Spec --------------------------------------------------------------------
8 | ;;
9 | ;; This is a clojure.spec specification for the value in app-db. It is like a
10 | ;; Schema. See: http://clojure.org/guides/spec
11 | ;;
12 | ;; The value in app-db should always match this spec. Only event handlers
13 | ;; can change the value in app-db so, after each event handler
14 | ;; has run, we re-check app-db for correctness (compliance with the Schema).
15 | ;;
16 | ;; How is this done? Look in events.cljs and you'll notice that all handlers
17 | ;; have an "after" interceptor which does the spec re-check.
18 | ;;
19 | ;; None of this is strictly necessary. It could be omitted. But we find it
20 | ;; good practice.
21 |
22 | (s/def ::id int?)
23 | (s/def ::title string?)
24 | (s/def ::done boolean?)
25 | (s/def ::todo (s/keys :req-un [::id ::title ::done]))
26 | (s/def ::todos (s/and ;; should use the :kind kw to s/map-of (not supported yet)
27 | (s/map-of ::id ::todo) ;; in this map, each todo is keyed by its :id
28 | #(instance? PersistentTreeMap %) ;; is a sorted-map (not just a map)
29 | ))
30 | (s/def ::showing ;; what todos are shown to the user?
31 | #{:all ;; all todos are shown
32 | :active ;; only todos whose :done is false
33 | :done ;; only todos whose :done is true
34 | })
35 | (s/def ::db (s/keys :req-un [::todos ::showing]))
36 |
37 | ;; -- Default app-db Value ---------------------------------------------------
38 | ;;
39 | ;; When the application first starts, this will be the value put in app-db
40 | ;; Unless, of course, there are todos in the LocalStore (see further below)
41 | ;; Look in:
42 | ;; 1. `core.cljs` for "(dispatch-sync [:initialise-db])"
43 | ;; 2. `events.cljs` for the registration of :initialise-db handler
44 | ;;
45 |
46 | (def default-db ;; what gets put into app-db by default.
47 | {:todos (sorted-map) ;; an empty list of todos. Use the (int) :id as the key
48 | :showing :all}) ;; show all todos
49 |
50 |
51 | ;; -- Local Storage ----------------------------------------------------------
52 | ;;
53 | ;; Part of the todomvc challenge is to store todos in LocalStorage, and
54 | ;; on app startup, reload the todos from when the program was last run.
55 | ;; But the challenge stipulates to NOT load the setting for the "showing"
56 | ;; filter. Just the todos.
57 | ;;
58 |
59 | (def ls-key "todos-reframe") ;; localstore key
60 |
61 | (defn todos->local-store
62 | "Puts todos into localStorage"
63 | [todos]
64 | (.setItem js/localStorage ls-key (str todos))) ;; sorted-map written as an EDN map
65 |
66 |
67 | ;; -- cofx Registrations -----------------------------------------------------
68 |
69 | ;; Use `reg-cofx` to register a "coeffect handler" which will inject the todos
70 | ;; stored in localstore.
71 | ;;
72 | ;; To see it used, look in `events.cljs` at the event handler for `:initialise-db`.
73 | ;; That event handler has the interceptor `(inject-cofx :local-store-todos)`
74 | ;; The function registered below will be used to fulfill that request.
75 | ;;
76 | ;; We must supply a `sorted-map` but in LocalStore it is stored as a `map`.
77 | ;;
78 | (re-frame/reg-cofx
79 | :local-store-todos
80 | (fn [cofx _]
81 | ;; put the localstore todos into the coeffect under :local-store-todos
82 | (assoc cofx :local-store-todos
83 | ;; read in todos from localstore, and process into a sorted map
84 | (into (sorted-map)
85 | (some->> (.getItem js/localStorage ls-key)
86 | (cljs.reader/read-string) ;; EDN map -> map
87 | )))))
88 |
--------------------------------------------------------------------------------
/examples/todomvc/src/todomvc/views.cljs:
--------------------------------------------------------------------------------
1 | (ns todomvc.views
2 | (:require [reagent.core :as reagent]
3 | [re-frame.core :refer [subscribe dispatch]]
4 | [clojure.string :as str]))
5 |
6 |
7 | (defn todo-input [{:keys [title on-save on-stop]}]
8 | (let [val (reagent/atom title)
9 | stop #(do (reset! val "")
10 | (when on-stop (on-stop)))
11 | save #(let [v (-> @val str str/trim)]
12 | (on-save v)
13 | (stop))]
14 | (fn [props]
15 | [:input (merge (dissoc props :on-save :on-stop :title)
16 | {:type "text"
17 | :value @val
18 | :auto-focus true
19 | :on-blur save
20 | :on-change #(reset! val (-> % .-target .-value))
21 | :on-key-down #(case (.-which %)
22 | 13 (save)
23 | 27 (stop)
24 | nil)})])))
25 |
26 |
27 | (defn todo-item
28 | []
29 | (let [editing (reagent/atom false)]
30 | (fn [{:keys [id done title]}]
31 | [:li {:class (str (when done "completed ")
32 | (when @editing "editing"))}
33 | [:div.view
34 | [:input.toggle
35 | {:type "checkbox"
36 | :checked done
37 | :on-change #(dispatch [:toggle-done id])}]
38 | [:label
39 | {:on-double-click #(reset! editing true)}
40 | title]
41 | [:button.destroy
42 | {:on-click #(dispatch [:delete-todo id])}]]
43 | (when @editing
44 | [todo-input
45 | {:class "edit"
46 | :title title
47 | :on-save #(if (seq %)
48 | (dispatch [:save id %])
49 | (dispatch [:delete-todo id]))
50 | :on-stop #(reset! editing false)}])])))
51 |
52 |
53 | (defn task-list
54 | []
55 | (let [visible-todos @(subscribe [:visible-todos])
56 | all-complete? @(subscribe [:all-complete?])]
57 | [:section#main
58 | [:input#toggle-all
59 | {:type "checkbox"
60 | :checked all-complete?
61 | :on-change #(dispatch [:complete-all-toggle])}]
62 | [:label
63 | {:for "toggle-all"}
64 | "Mark all as complete"]
65 | [:ul#todo-list
66 | (for [todo visible-todos]
67 | ^{:key (:id todo)} [todo-item todo])]]))
68 |
69 |
70 | (defn footer-controls
71 | []
72 | (let [[active done] @(subscribe [:footer-counts])
73 | showing @(subscribe [:showing])
74 | a-fn (fn [filter-kw txt]
75 | [:a {:class (when (= filter-kw showing) "selected")
76 | :href (str "#/" (name filter-kw))} txt])]
77 | [:footer#footer
78 | [:span#todo-count
79 | [:strong active] " " (case active 1 "item" "items") " left"]
80 | [:ul#filters
81 | [:li (a-fn :all "All")]
82 | [:li (a-fn :active "Active")]
83 | [:li (a-fn :done "Completed")]]
84 | (when (pos? done)
85 | [:button#clear-completed {:on-click #(dispatch [:clear-completed])}
86 | "Clear completed"])]))
87 |
88 |
89 | (defn task-entry
90 | []
91 | [:header#header
92 | [:h1 "todos"]
93 | [todo-input
94 | {:id "new-todo"
95 | :placeholder "What needs to be done?"
96 | :on-save #(when (seq %)
97 | (dispatch [:add-todo %]))}]])
98 |
99 |
100 | (defn todo-app
101 | []
102 | [:div
103 | [:section#todoapp
104 | [task-entry]
105 | (when (seq @(subscribe [:todos]))
106 | [task-list])
107 | [footer-controls]]
108 | [:footer#info
109 | [:p "Double-click to edit a todo"]]])
110 |
--------------------------------------------------------------------------------
/images/Readme/6dominoes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/Readme/6dominoes.png
--------------------------------------------------------------------------------
/images/Readme/Dominoes-small.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/Readme/Dominoes-small.jpg
--------------------------------------------------------------------------------
/images/Readme/Dominoes.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/Readme/Dominoes.jpg
--------------------------------------------------------------------------------
/images/Readme/todolist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/Readme/todolist.png
--------------------------------------------------------------------------------
/images/epoch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/epoch.png
--------------------------------------------------------------------------------
/images/event-handlers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/event-handlers.png
--------------------------------------------------------------------------------
/images/example_app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/example_app.png
--------------------------------------------------------------------------------
/images/logo/Genesis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/logo/Genesis.png
--------------------------------------------------------------------------------
/images/logo/Guggenheim.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/logo/Guggenheim.jpg
--------------------------------------------------------------------------------
/images/logo/README.md:
--------------------------------------------------------------------------------
1 |
2 | 
3 |
4 | [Read the backstory here.](/docs/The-re-frame-logo.md)
5 |
6 | Created via [Sketch.app](https://www.sketchapp.com/). See the file `re-frame-logo.sketch`
7 |
8 | Unfortunately the gradients are not exported properly so we can't provide an SVG here for now.
9 |
--------------------------------------------------------------------------------
/images/logo/frame_1024w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/logo/frame_1024w.png
--------------------------------------------------------------------------------
/images/logo/re-frame-logo.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/logo/re-frame-logo.sketch
--------------------------------------------------------------------------------
/images/logo/re-frame_128w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/logo/re-frame_128w.png
--------------------------------------------------------------------------------
/images/logo/re-frame_256w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/logo/re-frame_256w.png
--------------------------------------------------------------------------------
/images/logo/re-frame_512w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/logo/re-frame_512w.png
--------------------------------------------------------------------------------
/images/mental-model-omnibus.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/mental-model-omnibus.jpg
--------------------------------------------------------------------------------
/images/scale-changes-everything.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/scale-changes-everything.jpg
--------------------------------------------------------------------------------
/images/subscriptions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/subscriptions.png
--------------------------------------------------------------------------------
/images/the-water-cycle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/the-water-cycle.png
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | var root = 'run/compiled/karma/test' // same as :output-dir
3 | var junitOutputDir = process.env.CIRCLE_TEST_REPORTS || "run/compiled/karma/test/junit"
4 |
5 | config.set({
6 | frameworks: ['cljs-test'],
7 | browsers: ['ChromeHeadless'],
8 | basePath: './',
9 | files: [
10 | root + '/test.js'
11 | ],
12 | plugins: [
13 | 'karma-cljs-test',
14 | 'karma-chrome-launcher',
15 | 'karma-junit-reporter'
16 | ],
17 | colors: true,
18 | logLevel: config.LOG_INFO,
19 | client: {
20 | args: ['shadow.test.karma.init'],
21 | singleRun: true
22 | },
23 |
24 | // the default configuration
25 | junitReporter: {
26 | outputDir: junitOutputDir + '/karma', // results will be saved as outputDir/browserName.xml
27 | outputFile: undefined, // if included, results will be saved as outputDir/browserName/outputFile
28 | suite: '' // suite will become the package name attribute in xml testsuite element
29 | }
30 | })
31 | }
32 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015-2017 Michael Thompson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject re-frame "lein-git-inject/version"
2 | :description "A ClojureScript MVC-like Framework For Writing SPAs Using Reagent."
3 | :url "https://github.com/day8/re-frame.git"
4 | :license {:name "MIT"}
5 |
6 | :dependencies [[org.clojure/clojure "1.10.1" :scope "provided"]
7 | [org.clojure/clojurescript "1.10.597" :scope "provided"
8 | :exclusions [com.google.javascript/closure-compiler-unshaded
9 | org.clojure/google-closure-library
10 | org.clojure/google-closure-library-third-party]]
11 | [thheller/shadow-cljs "2.8.83" :scope "provided"]
12 | [reagent "0.9.1"]
13 | [net.cgrand/macrovich "0.2.1"]
14 | [org.clojure/tools.logging "0.4.1"]]
15 |
16 | :plugins [[day8/lein-git-inject "0.0.11"]
17 | [lein-shadow "0.1.7"]]
18 |
19 | :middleware [leiningen.git-inject/middleware]
20 |
21 | :git-inject {:version-pattern #"v(\d+\.\d+\.\d+.*)"}
22 |
23 | :profiles {:debug {:debug true}
24 | :dev {:dependencies [[binaryage/devtools "0.9.11"]]
25 | :plugins [[lein-ancient "0.6.15"]
26 | [lein-shell "0.5.0"]]}}
27 |
28 | :clean-targets [:target-path "run/compiled"]
29 |
30 | :resource-paths ["run/resources"]
31 | :jvm-opts ["-Xmx1g"]
32 | :source-paths ["src"]
33 | :test-paths ["test"]
34 |
35 | :shell {:commands {"open" {:windows ["cmd" "/c" "start"]
36 | :macosx "open"
37 | :linux "xdg-open"}}}
38 |
39 | :deploy-repositories [["clojars" {:sign-releases false
40 | :url "https://clojars.org/repo"
41 | :username :env/CLOJARS_USERNAME
42 | :password :env/CLOJARS_PASSWORD}]]
43 |
44 | :release-tasks [["deploy" "clojars"]]
45 |
46 | :shadow-cljs {:nrepl {:port 8777}
47 |
48 | :builds {:browser-test
49 | {:target :browser-test
50 | :ns-regexp "re-frame\\..*-test$"
51 | :test-dir "run/compiled/browser/test"
52 | :compiler-options {:pretty-print true
53 | :external-config {:devtools/config {:features-to-install [:formatters :hints]}}}
54 | :devtools {:http-port 3449
55 | :http-root "run/compiled/browser/test"
56 | :preloads [devtools.preload]}}
57 |
58 | :karma-test
59 | {:target :karma
60 | :ns-regexp "re-frame\\..*-test$"
61 | :output-to "run/compiled/karma/test/test.js"
62 | :compiler-options {:pretty-print true
63 | :closure-defines {re-frame.trace.trace-enabled? true}}}}}
64 |
65 | :aliases {"test-once" ["do" "clean," "shadow" "compile" "browser-test," "shell" "open" "run/compiled/browser/test/index.html"]
66 | "test-auto" ["do" "clean," "shadow" "watch" "browser-test,"]
67 | "karma-once" ["do"
68 | ["clean"]
69 | ["shadow" "compile" "karma-test"]
70 | ["shell" "karma" "start" "--single-run" "--reporters" "junit,dots"]]
71 | "karma-auto" ["do" "clean," "shadow" "watch" "karma-test,"]
72 | ;; NOTE: In order to compile docs you would need to install
73 | ;; gitbook-cli(2.3.2) utility globaly using npm or yarn
74 | "docs-serve" ^{:doc "Runs the development server of docs with live reloading"} ["shell" "gitbook" "serve" "./" "./build/re-frame/"]
75 | "docs-build" ^{:doc "Builds the HTML version of docs"} ["shell" "gitbook" "build" "./" "./build/re-frame/"]
76 | ;; NOTE: Calibre and svgexport(0.3.2) are needed to build below
77 | ;; formats of docs. Install svgexpor3t using npm or yarn.
78 | "docs-pdf" ^{:doc "Builds the PDF version of docs"}
79 | ["do"
80 | ["shell" "mkdir" "-p" "./build/"]
81 | ["shell" "gitbook" "pdf" "./" "./build/re-frame.pdf"]]
82 |
83 | "docs-mobi" ^{:doc "Builds the MOBI version of docs"}
84 | ["do"
85 | ["shell" "mkdir" "-p" "./build/"]
86 | ["shell" "gitbook" "mobi" "./" "./build/re-frame.mobi"]]
87 |
88 | "docs-epub" ^{:doc "Builds the EPUB version of docs"}
89 | ["do"
90 | ["shell" "mkdir" "-p" "./build/"]
91 | ["shell" "gitbook" "epub" "./" "./build/re-frame.epub"]]})
92 |
--------------------------------------------------------------------------------
/src/deps.cljs:
--------------------------------------------------------------------------------
1 | {:npm-dev-deps {"shadow-cljs" "2.8.83"
2 | "karma" "4.4.1"
3 | "karma-chrome-launcher" "3.1.0"
4 | "karma-cljs-test" "0.1.0"
5 | "karma-junit-reporter" "2.0.1"}}
6 |
--------------------------------------------------------------------------------
/src/re_frame/cofx.cljc:
--------------------------------------------------------------------------------
1 | (ns re-frame.cofx
2 | (:require [re-frame.interceptor :refer [->interceptor]]
3 | [re-frame.registry :as reg]
4 | [lambdaisland.glogi :as log]))
5 |
6 |
7 | ;; -- Registration ------------------------------------------------------------
8 |
9 | (def kind :cofx)
10 | (assert (re-frame.registry/kinds kind))
11 |
12 | ;; -- Interceptor -------------------------------------------------------------
13 |
14 | (defn inject-cofx
15 | "Given an `id`, and an optional, arbitrary `value`, returns an interceptor
16 | whose `:before` adds to the `:coeffects` (map) by calling a pre-registered
17 | 'coeffect handler' identified by the `id`.
18 |
19 | The previous association of a `coeffect handler` with an `id` will have
20 | happened via a call to `re-frame.core/reg-cofx` - generally on program startup.
21 |
22 | Within the created interceptor, this 'looked up' `coeffect handler` will
23 | be called (within the `:before`) with two arguments:
24 | - the current value of `:coeffects`
25 | - optionally, the originally supplied arbitrary `value`
26 |
27 | This `coeffect handler` is expected to modify and return its first, `coeffects` argument.
28 |
29 | Example Of how `inject-cofx` and `reg-cofx` work together
30 | ---------------------------------------------------------
31 |
32 | 1. Early in app startup, you register a `coeffect handler` for `:datetime`:
33 |
34 | (re-frame.core/reg-cofx
35 | :datetime ;; usage (inject-cofx :datetime)
36 | (fn coeffect-handler
37 | [coeffect]
38 | (assoc coeffect :now (js/Date.)))) ;; modify and return first arg
39 |
40 | 2. Later, add an interceptor to an -fx event handler, using `inject-cofx`:
41 |
42 | (re-frame.core/reg-event-fx ;; we are registering an event handler
43 | :event-id
44 | [ ... (inject-cofx :datetime) ... ] ;; <-- create an injecting interceptor
45 | (fn event-handler
46 | [coeffect event]
47 | ... in here can access (:now coeffect) to obtain current datetime ... )))
48 |
49 | Background
50 | ----------
51 |
52 | `coeffects` are the input resources required by an event handler
53 | to perform its job. The two most obvious ones are `db` and `event`.
54 | But sometimes an event handler might need other resources.
55 |
56 | Perhaps an event handler needs a random number or a GUID or the current
57 | datetime. Perhaps it needs access to a DataScript database connection.
58 |
59 | If an event handler directly accesses these resources, it stops being
60 | pure and, consequently, it becomes harder to test, etc. So we don't
61 | want that.
62 |
63 | Instead, the interceptor created by this function is a way to 'inject'
64 | 'necessary resources' into the `:coeffects` (map) subsequently given
65 | to the event handler at call time."
66 | ([registry id]
67 | (->interceptor
68 | :id :coeffects
69 | :before (fn coeffects-before
70 | [context]
71 | (if-let [handler (reg/get-handler registry kind id)]
72 | (update context :coeffects handler (:frame context))
73 | (log/error :missing-cofx-handler {:id id})))))
74 | ([registry id value]
75 | (->interceptor
76 | :id :coeffects
77 | :before (fn coeffects-before
78 | [context]
79 | (if-let [handler (reg/get-handler registry kind id)]
80 | (update context :coeffects handler value (:frame context))
81 | (log/error :missing-cofx-handler {:id id}))))))
82 |
83 |
84 | ;; -- Builtin CoEffects Handlers ---------------------------------------------
85 |
86 | (defn register-built-in!
87 | [{:keys [registry]}]
88 | (let [reg-cofx (partial reg/register-handler registry kind)]
89 | (reg-cofx
90 | :db
91 | (fn db-coeffects-handler
92 | [coeffects frame]
93 | (assoc coeffects :db @(:app-db frame))))))
94 |
95 | ;; Because this interceptor is used so much, we reify it
96 | ;; (def inject-db (inject-cofx :db))
97 |
--------------------------------------------------------------------------------
/src/re_frame/context.clj:
--------------------------------------------------------------------------------
1 | (ns re-frame.context
2 | (:refer-clojure :exclude [bound-fn])
3 | (:require [cljs.env]
4 | [cljs.analyzer]))
5 |
6 | (defmacro defc
7 | "For definining Reagent components that honor the contextual frame. Like defn
8 | but sets a :context-type metadata on the function, which Reagent will pick up
9 | on, so that the correct React context is set for this component."
10 | [name & fntail]
11 | (let [[doc fntail] (if (string? (first fntail))
12 | [(first fntail) (rest fntail)]
13 | [nil fntail])]
14 | `(def ~(with-meta name (merge {:doc doc} (:meta &form)))
15 | ^{:context-type frame-context}
16 | (fn ~@fntail))))
17 |
18 | (defmacro bind-frame [frame & body]
19 | `(binding [~'re-frame.registry/*current-frame* ~frame]
20 | (assert (satisfies? ~'re-frame.frame/IFrame ~frame) "given frame is not of type `re-frame.frame/IFrame`")
21 | ~@body))
22 |
23 | (defmacro import-with-frame
24 | ([var-sym]
25 | `(import-with-frame ~(symbol (name var-sym)) ~var-sym))
26 | ([name var-sym]
27 | `(defn ~name
28 | ;; Attempt at propagating the doc string / arglists, for some reason CIDER
29 | ;; is not picking this up though.
30 | ~(select-keys (:meta (cljs.analyzer/resolve-var cljs.env/*compiler* var-sym))
31 | [:doc :arglists])
32 | [& args#]
33 | (apply ~var-sym (current-frame) args#))))
34 |
35 | (defmacro bound-fn [& args]
36 | (let [[name argv & body] (if (symbol? (first args))
37 | args
38 | (into [nil] args))]
39 | `(let [frame# (~'re-frame.context/current-frame)]
40 | (fn ~@(when name name) ~argv
41 | (binding [~'re-frame.registry/*current-frame* frame#]
42 | ~@body)))))
43 |
--------------------------------------------------------------------------------
/src/re_frame/context.cljs:
--------------------------------------------------------------------------------
1 | (ns re-frame.context
2 | (:require ["react" :as react]
3 | [goog.object :as gobj]
4 | [lambdaisland.glogi :as log]
5 | [re-frame.core :as r]
6 | [re-frame.frame :as frame]
7 | [re-frame.registry :as registry]
8 | [re-frame.subs :as subs]
9 | [reagent.core])
10 | (:require-macros [re-frame.context :refer [defc import-with-frame]]))
11 |
12 | (def frame-context (.createContext react r/default-frame))
13 |
14 | (defn set-default-frame [frame]
15 | (gobj/set frame-context "_currentValue" frame)
16 | (gobj/set frame-context "_currentValue2" frame))
17 |
18 | (defn current-context
19 | "Gets the react Context for the current component, to be used in lifecycle
20 | hooks (e.g. render). Assumes that Component.contextType has been set."
21 | []
22 | (when-let [cmp (reagent.core/current-component)]
23 | ;; When used without setting the right contextType we will get #js {} back
24 | (when (not (object? (.-context cmp)))
25 | (.-context cmp))))
26 |
27 | (defn current-frame
28 | "Get the current frame provided by the context, falling back to the default
29 | frame. Assumes that Component.contextType = frame-context."
30 | []
31 | (or registry/*current-frame*
32 | (current-context)
33 | (gobj/get frame-context "_currentValue")))
34 |
35 | (defn bound-frame []
36 | (or registry/*current-frame*
37 | (current-context)
38 | (throw (js/Error. "No frame bound"))))
39 |
40 | (defn provide-frame
41 | "Component that acts as a provider for the frame, so to run an isolated version
42 | of your app, use.
43 |
44 | [provide-frame (frame/make-frame)
45 | [app]]"
46 | [frame & children]
47 | (reagent.core/create-element
48 | (.-Provider frame-context)
49 | #js {:value frame
50 | :children (reagent.core/as-element (into [:<>] children))}))
51 |
52 | (defc provide-app-db
53 | "Component that acts as a provider for the app-db, it takes the registry from
54 | the current frame, but uses the given atom for the app-db"
55 | [app-db & children]
56 | `[~provide-frame ~(frame/make-frame {:registry (:registry (current-frame))
57 | :app-db app-db})
58 | ~@children])
59 |
60 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
61 | ;; Complete copy of the top-level re-frame API. If you are using the context
62 | ;; approach then import re-frame.context instead of re-frame.core and things
63 | ;; should generally Just Work™
64 |
65 | (import-with-frame subscribe re-frame.frame/subscribe)
66 | (import-with-frame dispatch re-frame.frame/dispatch)
67 | (import-with-frame dispatch-sync re-frame.frame/dispatch-sync)
68 | (import-with-frame clear-sub re-frame.frame/clear-sub)
69 | (import-with-frame reg-fx re-frame.frame/reg-fx)
70 | (import-with-frame reg-cofx re-frame.frame/reg-cofx)
71 | (import-with-frame inject-cofx re-frame.frame/inject-cofx)
72 | (import-with-frame clear-cofx re-frame.frame/clear-cofx)
73 | (import-with-frame reg-event-db re-frame.frame/reg-event-db)
74 | (import-with-frame reg-event-fx re-frame.frame/reg-event-fx)
75 | (import-with-frame reg-event-ctx re-frame.frame/reg-event-ctx)
76 | (import-with-frame clear-event re-frame.frame/clear-event)
77 |
78 | ;; A few special cases which we can't import directly
79 |
80 | (defn reg-sub-raw [query-id handler-fn]
81 | (frame/reg-sub-raw
82 | (current-frame)
83 | query-id
84 | (fn [frame query-v]
85 | (handler-fn (:app-db frame) query-v))))
86 |
87 | ;; some slight weirdness here because protocols don't support variadic functions
88 | (defn reg-sub [query-id & args]
89 | (frame/reg-sub (current-frame) query-id args))
90 |
91 | (defn clear-subscriptions-cache! [& args]
92 | (apply subs/-clear (:subs-cache (current-frame)) args))
93 |
94 |
95 | (defn context-fns
96 | "Returns subscribe/dispatch/dispatch-sync functions that are bound to the current frame. Use like this
97 |
98 | (defc my-component []
99 | (reagent/with-let [{:keys [subscribe dispatch]} (re-frame/context-fns)]
100 | ,,,
101 | )) "
102 | ([] (context-fns (current-frame)))
103 | ([frame]
104 | {:subscribe (partial re-frame.frame/subscribe frame)
105 | :dispatch (partial re-frame.frame/dispatch frame)
106 | :dispatch-sync (partial re-frame.frame/dispatch-sync frame)}))
107 |
108 | (defn bind-fn [f]
109 | (let [frame (current-frame)]
110 | (fn [& args]
111 | (binding [registry/*current-frame* frame]
112 | (apply f args)))))
113 |
--------------------------------------------------------------------------------
/src/re_frame/core.cljc:
--------------------------------------------------------------------------------
1 | (ns re-frame.core
2 | (:require [re-frame.events :as events]
3 | [re-frame.subs :as subs]
4 | [re-frame.frame :as frame]
5 | [re-frame.interop :as interop]
6 | [re-frame.fx :as fx]
7 | [re-frame.cofx :as cofx]
8 | [re-frame.router :as router]
9 | [re-frame.loggers :as loggers]
10 | [re-frame.registry :as reg]
11 | [re-frame.interceptor :as interceptor]
12 | [re-frame.std-interceptors :as std-interceptors]))
13 |
14 | ;; -- API ---------------------------------------------------------------------
15 | ;;
16 | ;; This namespace represents the re-frame API
17 | ;;
18 | ;; Below, you'll see we've used this technique:
19 | ;; (def api-name-for-fn deeper.namespace/where-the-defn-is)
20 | ;;
21 | ;; So, we promote a `defn` in a deeper namespace "up" to the API
22 | ;; via a `def` in this namespace.
23 | ;;
24 | ;; Turns out, this approach makes it hard:
25 | ;; - to auto-generate API docs
26 | ;; - for IDEs to provide code completion on functions in the API
27 | ;;
28 | ;; Which is annoying. But there are pros and cons and we haven't
29 | ;; yet revisited the decision. To compensate, we've added more nudity
30 | ;; to the docs.
31 | ;;
32 |
33 | ;; -- interceptor related
34 | ;; useful if you are writing your own interceptors
35 | (def ->interceptor interceptor/->interceptor)
36 | (def enqueue interceptor/enqueue)
37 | (def get-coeffect interceptor/get-coeffect)
38 | (def get-effect interceptor/get-effect)
39 | (def assoc-effect interceptor/assoc-effect)
40 | (def assoc-coeffect interceptor/assoc-coeffect)
41 |
42 |
43 | ;; -- standard interceptors
44 | (def debug std-interceptors/debug)
45 | (def path std-interceptors/path)
46 | (def enrich std-interceptors/enrich)
47 | (def trim-v std-interceptors/trim-v)
48 | (def after std-interceptors/after)
49 | (def on-changes std-interceptors/on-changes)
50 |
51 | ;; XXX move API functions up to this core level - to enable code completion and docs
52 | ;; XXX on figwheel reload, is there a way to not get the re-registration messages.
53 |
54 | ;; Export API wrapping `default-frame` singleton ---
55 |
56 | (def default-frame (frame/make-frame))
57 |
58 | (fx/register-built-in! default-frame)
59 | (cofx/register-built-in! default-frame)
60 |
61 | (def dispatch (partial frame/dispatch default-frame))
62 | (def dispatch-sync (partial frame/dispatch-sync default-frame))
63 |
64 | (defn reg-sub-raw [query-id handler-fn]
65 | (frame/reg-sub-raw
66 | default-frame
67 | query-id
68 | (fn [frame query-v]
69 | (handler-fn (:app-db frame) query-v))))
70 |
71 | ;; some slight weirdness here because protocols don't support variadic functions
72 | (defn reg-sub [query-id & args]
73 | (frame/reg-sub default-frame query-id args))
74 |
75 | (def subscribe (partial frame/subscribe default-frame))
76 | (def clear-sub (partial frame/clear-sub default-frame))
77 | (def clear-subscriptions-cache! (partial subs/-clear (:subs-cache default-frame)))
78 |
79 | (def reg-fx (partial frame/reg-fx default-frame))
80 | (def clear-fx (partial frame/clear-fx default-frame))
81 |
82 | (def reg-cofx (partial frame/reg-cofx default-frame))
83 | (def inject-cofx (partial frame/inject-cofx default-frame))
84 | (def clear-cofx (partial frame/clear-cofx default-frame))
85 |
86 | (def reg-event-db (partial frame/reg-event-db default-frame))
87 | (def reg-event-fx (partial frame/reg-event-fx default-frame))
88 | (def reg-event-ctx (partial frame/reg-event-ctx default-frame))
89 | (def clear-event (partial frame/clear-event default-frame))
90 |
91 | ;; -- logging ----------------------------------------------------------------
92 | ;; Internally, re-frame uses the logging functions: warn, log, error, group and groupEnd
93 | ;; By default, these functions map directly to the js/console implementations,
94 | ;; but you can override with your own fns (set or subset).
95 | ;; Example Usage:
96 | ;; (defn my-fn [& args] (post-it-somewhere (apply str args))) ;; here is my alternative
97 | ;; (re-frame.core/set-loggers! {:warn my-fn :log my-fn}) ;; override the defaults with mine
98 | (def set-loggers! loggers/set-loggers!)
99 |
100 | ;; If you are writing an extension to re-frame, like perhaps
101 | ;; an effects handler, you may want to use re-frame logging.
102 | ;;
103 | ;; usage: (console :error "Oh, dear God, it happened: " a-var " and " another)
104 | ;; (console :warn "Possible breach of containment wall at: " dt)
105 | (def console loggers/console)
106 |
107 |
108 | ;; -- unit testing ------------------------------------------------------------
109 |
110 | (defn make-restore-fn
111 | "Checkpoints the state of re-frame and returns a function which, when
112 | later called, will restore re-frame to that checkpointed state.
113 |
114 | Checkpoint includes app-db, all registered handlers and all subscriptions."
115 | []
116 | (frame/make-restore-fn default-frame))
117 |
118 | (defn purge-event-queue
119 | "Remove all events queued for processing"
120 | []
121 | (router/purge (:event-queue default-frame)))
122 |
123 | ;; -- Event Processing Callbacks ---------------------------------------------
124 |
125 | (defn add-post-event-callback
126 | "Registers a function `f` to be called after each event is processed
127 | `f` will be called with two arguments:
128 | - `event`: a vector. The event just processed.
129 | - `queue`: a PersistentQueue, possibly empty, of events yet to be processed.
130 |
131 | This is useful in advanced cases like:
132 | - you are implementing a complex bootstrap pipeline
133 | - you want to create your own handling infrastructure, with perhaps multiple
134 | handlers for the one event, etc. Hook in here.
135 | - libraries providing 'isomorphic javascript' rendering on Nodejs or Nashorn.
136 |
137 | 'id' is typically a keyword. Supplied at \"add time\" so it can subsequently
138 | be used at \"remove time\" to get rid of the right callback."
139 | ([f]
140 | (add-post-event-callback f f)) ;; use f as its own identifier
141 | ([id f]
142 | (router/add-post-event-callback (:event-queue default-frame) id f)))
143 |
144 |
145 | (defn remove-post-event-callback
146 | [id]
147 | (router/remove-post-event-callback (:event-queue default-frame) id))
148 |
--------------------------------------------------------------------------------
/src/re_frame/events.cljc:
--------------------------------------------------------------------------------
1 | (ns re-frame.events
2 | (:require [re-frame.utils :refer [first-in-vector]]
3 | [re-frame.interop :refer [empty-queue debug-enabled?]]
4 | [re-frame.registry :as reg]
5 | [re-frame.loggers :refer [console]]
6 | [re-frame.interceptor :as interceptor]
7 | [re-frame.trace :as trace :include-macros true]))
8 |
9 |
10 | (def kind :event)
11 | (assert (re-frame.registry/kinds kind))
12 |
13 | (defn- flatten-and-remove-nils
14 | "`interceptors` might have nested collections, and contain nil elements.
15 | return a flat collection, with all nils removed.
16 | This function is 9/10 about giving good error messages."
17 | [id interceptors]
18 | (let [make-chain #(->> % flatten (remove nil?))]
19 | (if-not debug-enabled?
20 | (make-chain interceptors)
21 | (do ;; do a whole lot of development time checks
22 | (when-not (coll? interceptors)
23 | (console :error "re-frame: when registering" id ", expected a collection of interceptors, got:" interceptors))
24 | (let [chain (make-chain interceptors)]
25 | (when (empty? chain)
26 | (console :error "re-frame: when registering" id ", given an empty interceptor chain"))
27 | (when-let [not-i (first (remove interceptor/interceptor? chain))]
28 | (if (fn? not-i)
29 | (console :error "re-frame: when registering" id ", got a function instead of an interceptor. Did you provide old style middleware by mistake? Got:" not-i)
30 | (console :error "re-frame: when registering" id ", expected interceptors, but got:" not-i)))
31 | chain)))))
32 |
33 |
34 | (defn register
35 | "Associate the given event `id` with the given collection of `interceptors`.
36 |
37 | `interceptors` may contain nested collections and there may be nils
38 | at any level,so process this structure into a simple, nil-less vector
39 | before registration.
40 |
41 | Typically, an `event handler` will be at the end of the chain (wrapped
42 | in an interceptor)."
43 | [registry id interceptors]
44 | (reg/register-handler registry kind id (flatten-and-remove-nils id interceptors)))
45 |
46 |
47 |
48 | ;; -- handle event --------------------------------------------------------------------------------
49 |
50 | (def ^:dynamic *handling* nil) ;; remember what event we are currently handling
51 |
52 | (defn handle
53 | "Given an event vector `event-v`, look up the associated interceptor chain, and execute it."
54 | [{:keys [registry app-db] :as frame} event-v]
55 | (let [event-id (first-in-vector event-v)]
56 | (when-let [interceptors (reg/get-handler registry kind event-id true)]
57 | (if *handling*
58 | (console :error "re-frame: while handling" *handling* ", dispatch-sync was called for" event-v ". You can't call dispatch-sync within an event handler.")
59 | (binding [*handling* event-v]
60 | (trace/with-trace {:operation event-id
61 | :op-type kind
62 | :tags {:event event-v}}
63 | (trace/merge-trace! {:tags {:app-db-before @app-db}})
64 | (interceptor/execute frame event-v interceptors)
65 | (trace/merge-trace! {:tags {:app-db-after @app-db}})))))))
66 |
--------------------------------------------------------------------------------
/src/re_frame/frame.cljc:
--------------------------------------------------------------------------------
1 | (ns re-frame.frame
2 | "An instance of freerange's state.
3 |
4 | A frame combines all of freerange's state in a single object, so it is
5 | possible to have multiple, isolated instances. "
6 | (:require [re-frame.cofx :as cofx]
7 | [re-frame.events :as events]
8 | [re-frame.fx :as fx]
9 | [re-frame.interop :as interop]
10 | [re-frame.registry :as reg]
11 | [re-frame.router :as router]
12 | [re-frame.std-interceptors :as stdi]
13 | [re-frame.subs :as subs]))
14 |
15 | (defprotocol IFrame
16 | ;; dispatch ----
17 | (dispatch [this event-v])
18 | (dispatch-sync [this event-v])
19 |
20 | ;; subs ----
21 | (reg-sub-raw [this query-id handler-fn])
22 | (reg-sub [this query-id args])
23 | (subscribe [this query-v])
24 | (clear-sub [this query-id])
25 | (clear-subscriptions-cache [this])
26 |
27 | ;; fx ----
28 | (reg-fx [this fx-id handler-fn])
29 | (clear-fx [this fx-id])
30 |
31 | ;; cofx ----
32 | (reg-cofx [this cofx-id handler-fn])
33 | (inject-cofx
34 | [this cofx-id]
35 | [this cofx-id value])
36 | (clear-cofx [this cofx-id])
37 |
38 | ;; events ----
39 | (clear-event [this event-id])
40 | (reg-event-db
41 | [this id db-handler]
42 | [this id interceptors db-handler])
43 | (reg-event-fx
44 | [this id fx-handler]
45 | [this id interceptors fx-handler])
46 | (reg-event-ctx
47 | [this id handler]
48 | [this id interceptors handler]))
49 |
50 | ;; connect all the pieces of state ----
51 | (defrecord Frame [registry event-queue app-db subs-cache default-interceptors]
52 | IFrame
53 | ;; dispatch ----
54 | (dispatch [this event-v]
55 | (router/dispatch event-queue event-v))
56 | (dispatch-sync [this event-v]
57 | (router/dispatch-sync this event-v))
58 |
59 | ;; subs ----
60 | (reg-sub-raw [this query-id handler-fn]
61 | (reg/register-handler registry subs/kind query-id handler-fn))
62 | (reg-sub [this query-id args]
63 | (apply subs/reg-sub this query-id args))
64 | (subscribe [this query-v]
65 | (subs/subscribe this query-v))
66 | (clear-sub [this query-id]
67 | (reg/clear-handlers registry subs/kind query-id))
68 | (clear-subscriptions-cache [this]
69 | (subs/clear-subscription-cache! subs-cache))
70 |
71 | ;; fx ----
72 | (reg-fx [this fx-id handler-fn]
73 | (reg/register-handler registry fx/kind fx-id handler-fn))
74 | (clear-fx [this fx-id]
75 | (reg/clear-handlers registry fx/kind fx-id))
76 |
77 | ;; cofx ----
78 | (reg-cofx [this cofx-id handler-fn]
79 | (reg/register-handler registry cofx/kind cofx-id handler-fn))
80 | (inject-cofx [this cofx-id]
81 | (cofx/inject-cofx registry cofx-id))
82 | (inject-cofx [this cofx-id value]
83 | (cofx/inject-cofx registry cofx-id value))
84 | (clear-cofx [this cofx-id]
85 | (reg/clear-handlers registry cofx/kind cofx-id))
86 |
87 | ;; events ----
88 | (clear-event [this id]
89 | (reg/clear-handlers registry events/kind id))
90 |
91 | (reg-event-db [this id db-handler]
92 | (reg-event-db this id nil db-handler))
93 | (reg-event-db [this id interceptors db-handler]
94 | (events/register
95 | registry
96 | id
97 | [default-interceptors interceptors (stdi/db-handler->interceptor db-handler)]))
98 | (reg-event-fx [this id fx-handler]
99 | (reg-event-fx this id nil fx-handler))
100 | (reg-event-fx [this id interceptors fx-handler]
101 | (events/register
102 | registry
103 | id
104 | [default-interceptors interceptors (stdi/fx-handler->interceptor fx-handler)]))
105 | (reg-event-ctx [this id handler]
106 | (reg-event-ctx this id nil handler))
107 | (reg-event-ctx [this id interceptors handler]
108 | (events/register
109 | registry
110 | id
111 | [default-interceptors interceptors (stdi/ctx-handler->interceptor handler)])))
112 |
113 | (def frame-id (atom 0))
114 |
115 | (defn make-frame
116 | "Creates a new frame, which bundles the registry (subscriptions, event-handlers,
117 | fx, cofx), app-db, subscription cache, default interceptors, and event queue.
118 |
119 | :registry, :app-db, and :interceptors can be provided through an options map."
120 | [& [{:keys [registry app-db interceptors] :as extra-keys}]]
121 | (let [registry (or registry (reg/make-registry))
122 | app-db (or app-db (interop/ratom {}))
123 | default-interceptors [(cofx/inject-cofx registry :db)
124 | (fx/do-fx registry)]
125 | frame (map->Frame
126 | (merge {:frame-id (swap! frame-id inc)
127 | :registry registry
128 | :app-db app-db
129 | :subs-cache (subs/->SubscriptionCache (atom {}))
130 | :default-interceptors (if interceptors
131 | (if (:replace (meta interceptors))
132 | interceptors
133 | (into default-interceptors interceptors))
134 | default-interceptors)
135 | :event-queue (router/->EventQueue :idle interop/empty-queue {} nil)}
136 | (dissoc extra-keys :registry :app-db :interceptors)))]
137 | ;; When events / fx fire, they get their frame from the event-queue
138 | (set! (.-frame (:event-queue frame)) frame)
139 | frame))
140 |
141 | (defn make-restore-fn
142 | "Checkpoints the state of re-frame and returns a function which, when
143 | later called, will restore re-frame to that checkpointed state.
144 |
145 | Checkpoint includes app-db, all registered handlers and all subscriptions."
146 | ([frame]
147 | (let [handlers (-> frame :registry :kind->id->handler deref)
148 | app-db (-> frame :app-db deref)
149 | subs-cache (-> frame :subs-cache deref)]
150 | (fn []
151 | ;; call `dispose!` on all current subscriptions which
152 | ;; didn't originally exist.
153 | (let [original-subs (-> subs-cache vals set)
154 | current-subs (-> frame :subs-cache deref vals)]
155 | (doseq [sub current-subs
156 | :when (not (contains? original-subs sub))]
157 | (interop/dispose! sub)))
158 |
159 | ;; Reset the atoms
160 | ;; We don't need to reset subs-cache, as disposing of the subs
161 | ;; removes them from the cache anyway
162 | (reset! (-> frame :registry :kind->id->handler) handlers)
163 | (reset! (-> frame :app-db) app-db)
164 | nil))))
165 |
--------------------------------------------------------------------------------
/src/re_frame/fx.cljc:
--------------------------------------------------------------------------------
1 | (ns re-frame.fx
2 | (:require [re-frame.router :as router]
3 | [re-frame.interceptor :refer [->interceptor]]
4 | [re-frame.interop :refer [set-timeout!]]
5 | [re-frame.events :as events]
6 | [re-frame.registry :as reg]
7 | [re-frame.loggers :refer [console]]
8 | [re-frame.trace :as trace :include-macros true]
9 | [lambdaisland.glogi :as log]))
10 |
11 | ;; -- Registration ------------------------------------------------------------
12 |
13 | (def kind :fx)
14 | (assert (re-frame.registry/kinds kind))
15 |
16 | ;; -- Interceptor -------------------------------------------------------------
17 |
18 | (defn register-built-in!
19 | [{:keys [registry]}]
20 | (let [reg-fx (partial reg/register-handler registry kind)]
21 |
22 | ;; :dispatch-later
23 | ;;
24 | ;; `dispatch` one or more events after given delays. Expects a collection
25 | ;; of maps with two keys: :`ms` and `:dispatch`
26 | ;;
27 | ;; usage:
28 | ;;
29 | ;; {:dispatch-later [{:ms 200 :dispatch [:event-id "param"]} ;; in 200ms do this: (dispatch [:event-id "param"])
30 | ;; {:ms 100 :dispatch [:also :this :in :100ms]}]}
31 | ;;
32 | ;; Note: nil entries in the collection are ignored which means events can be added
33 | ;; conditionally:
34 | ;; {:dispatch-later [ (when (> 3 5) {:ms 200 :dispatch [:conditioned-out]})
35 | ;; {:ms 100 :dispatch [:another-one]}]}
36 | ;;
37 | (reg-fx
38 | :dispatch-later
39 | (fn [value {:keys [event-queue]}]
40 | (doseq [{:keys [ms dispatch] :as effect} (remove nil? value)]
41 | (if (or (empty? dispatch) (not (number? ms)))
42 | (console :error "re-frame: ignoring bad :dispatch-later value:" effect)
43 | (set-timeout! #(router/dispatch event-queue dispatch) ms)))))
44 |
45 |
46 | ;; :dispatch
47 | ;;
48 | ;; `dispatch` one event. Expects a single vector.
49 | ;;
50 | ;; usage:
51 | ;; {:dispatch [:event-id "param"] }
52 |
53 | (reg-fx
54 | :dispatch
55 | (fn [value {:keys [event-queue]}]
56 | (if-not (vector? value)
57 | (console :error "re-frame: ignoring bad :dispatch value. Expected a vector, but got:" value)
58 | (router/dispatch event-queue value))))
59 |
60 |
61 | ;; :dispatch-n
62 | ;;
63 | ;; `dispatch` more than one event. Expects a list or vector of events. Something for which
64 | ;; sequential? returns true.
65 | ;;
66 | ;; usage:
67 | ;; {:dispatch-n (list [:do :all] [:three :of] [:these])}
68 | ;;
69 | ;; Note: nil events are ignored which means events can be added
70 | ;; conditionally:
71 | ;; {:dispatch-n (list (when (> 3 5) [:conditioned-out])
72 | ;; [:another-one])}
73 | ;;
74 | (reg-fx
75 | :dispatch-n
76 | (fn [value {:keys [event-queue]}]
77 | (if-not (sequential? value)
78 | (console :error "re-frame: ignoring bad :dispatch-n value. Expected a collection, but got:" value)
79 | (doseq [event (remove nil? value)] (router/dispatch event-queue event)))))
80 |
81 |
82 | ;; :deregister-event-handler
83 | ;;
84 | ;; removes a previously registered event handler. Expects either a single id (
85 | ;; typically a namespaced keyword), or a seq of ids.
86 | ;;
87 | ;; usage:
88 | ;; {:deregister-event-handler :my-id)}
89 | ;; or:
90 | ;; {:deregister-event-handler [:one-id :another-id]}
91 | ;;
92 | (reg-fx
93 | :deregister-event-handler
94 | (fn [value {:keys [registry]}]
95 | (let [clear-event (partial reg/clear-handlers registry events/kind)]
96 | (if (sequential? value)
97 | (doseq [event value] (clear-event event))
98 | (clear-event value)))))
99 |
100 |
101 | ;; :db
102 | ;;
103 | ;; reset! app-db with a new value. `value` is expected to be a map.
104 | ;;
105 | ;; usage:
106 | ;; {:db {:key1 value1 key2 value2}}
107 | ;;
108 | (reg-fx
109 | :db
110 | (fn [value {:keys [app-db]}]
111 | (when-not (identical? @app-db value)
112 | (reset! app-db value))))))
113 |
114 | ;; -- Builtin Effect Handlers ------------------------------------------------
115 |
116 | (defn do-fx
117 | "An interceptor whose `:after` actions the contents of `:effects`. As a result
118 | this interceptor is Domino 3.
119 |
120 | This interceptor is silently added (by reg-event-db etc) to the front of
121 | interceptor chains for all events.
122 |
123 | For each key in `:effects` (a map), it calls the registered `effects handler`
124 | (see `reg-fx` for registration of effect handlers).
125 |
126 | So, if `:effects` was:
127 | {:dispatch [:hello 42]
128 | :db {...}
129 | :undo \"set flag\"}
130 |
131 | it will call the registered effect handlers for each of the map's keys:
132 | `:dispatch`, `:undo` and `:db`. When calling each handler, provides the map
133 | value for that key - so in the example above the effect handler for :dispatch
134 | will be given one arg `[:hello 42]`.
135 |
136 | You cannot rely on the ordering in which effects are executed."
137 | [registry]
138 | (->interceptor
139 | :id :do-fx
140 | :after (fn do-fx-after
141 | [context]
142 | {:pre [(:frame context)]}
143 | (trace/with-trace
144 | {:op-type :event/do-fx}
145 | (doseq [[effect-key effect-value] (:effects context)]
146 | (if-let [effect-fn (reg/get-handler registry kind effect-key)]
147 | (effect-fn effect-value (:frame context))
148 | (log/error :missing-fx-handler {:effect-key effect-key})))))))
149 |
--------------------------------------------------------------------------------
/src/re_frame/interop.clj:
--------------------------------------------------------------------------------
1 | (ns re-frame.interop
2 | (:import [java.util.concurrent Executor Executors]))
3 |
4 | ;; The purpose of this file is to provide JVM-runnable implementations of the
5 | ;; CLJS equivalents in interop.cljs.
6 | ;;
7 | ;; These implementations are to enable you to bring up a re-frame app on the JVM
8 | ;; in order to run tests, or to develop at a JVM REPL instead of a CLJS one.
9 | ;;
10 | ;; Please note, though, that the purpose here *isn't* to fully replicate all of
11 | ;; re-frame's behaviour in a real CLJS environment. We don't have Reagent or
12 | ;; React on the JVM, and we don't try to mimic the stateful lifecycles that they
13 | ;; embody.
14 | ;;
15 | ;; In particular, if you're performing side effects in any code that's triggered
16 | ;; by a change to a Ratom's value, and not via a call to `dispatch`, then you're
17 | ;; going to have a hard time getting any accurate tests with this code.
18 | ;; However, if your subscriptions and Reagent render functions are pure, and
19 | ;; your side-effects are all managed by effect handlers, then hopefully this will
20 | ;; allow you to write some useful tests that can run on the JVM.
21 |
22 |
23 | (defonce ^:private executor (Executors/newSingleThreadExecutor))
24 |
25 | (defonce ^:private on-dispose-callbacks (atom {}))
26 |
27 | (defn next-tick [f]
28 | (let [bound-f (bound-fn [& args] (apply f args))]
29 | (.execute ^Executor executor bound-f))
30 | nil)
31 |
32 | (def empty-queue clojure.lang.PersistentQueue/EMPTY)
33 |
34 | (def after-render next-tick)
35 |
36 | (def debug-enabled? true)
37 |
38 | (defn ratom [x]
39 | (atom x))
40 |
41 | (defn ratom? [x]
42 | (instance? clojure.lang.IAtom x))
43 |
44 | (defn deref? [x]
45 | (instance? clojure.lang.IDeref x))
46 |
47 | (defn make-reaction
48 | "On JVM Clojure, return a `deref`-able thing which invokes the given function
49 | on every `deref`. That is, `make-reaction` here provides precisely none of the
50 | benefits of `reagent.ratom/make-reaction` (which only invokes its function if
51 | the reactions that the function derefs have changed value). But so long as `f`
52 | only depends on other reactions (which also behave themselves), the only
53 | difference is one of efficiency. That is, your tests should see no difference
54 | other than that they do redundant work."
55 | [f]
56 | (reify clojure.lang.IDeref
57 | (deref [_] (f))))
58 |
59 | (defn add-on-dispose!
60 | "On JVM Clojure, use an atom to register `f` to be invoked when `dispose!` is
61 | invoked with `a-ratom`."
62 | [a-ratom f]
63 | (swap! on-dispose-callbacks update a-ratom (fnil conj []) f)
64 | nil)
65 |
66 | (defn dispose!
67 | "On JVM Clojure, invoke all callbacks registered with `add-on-dispose!` for
68 | `a-ratom`."
69 | [a-ratom]
70 | ;; Try to replicate reagent's behavior, releasing resources first then
71 | ;; invoking callbacks
72 | (let [callbacks (get @on-dispose-callbacks a-ratom)]
73 | (swap! on-dispose-callbacks dissoc a-ratom)
74 | (doseq [f callbacks] (f))))
75 |
76 | (defn set-timeout!
77 | "Note that we ignore the `ms` value and just invoke the function, because
78 | there isn't often much point firing a timed event in a test."
79 | [f ms]
80 | (next-tick f))
81 |
82 | (defn now []
83 | ;; currentTimeMillis may count backwards in some scenarios, but as this is used for tracing
84 | ;; it is preferable to the slower but more accurate System.nanoTime.
85 | (System/currentTimeMillis))
86 |
87 | (defn reagent-id
88 | "Doesn't make sense in a Clojure context currently."
89 | [reactive-val]
90 | "rx-clj")
91 |
--------------------------------------------------------------------------------
/src/re_frame/interop.cljs:
--------------------------------------------------------------------------------
1 | (ns re-frame.interop
2 | (:require [reagent.core]
3 | [reagent.impl.batching]
4 | [reagent.ratom]
5 | [react :as react]
6 | [react-dom :as react-dom]))
7 |
8 | (def next-tick reagent.impl.batching/do-before-flush)
9 |
10 | (def empty-queue #queue [])
11 |
12 | (def after-render reagent.core/after-render)
13 |
14 | ;; Make sure the Google Closure compiler sees this as a boolean constant,
15 | ;; otherwise Dead Code Elimination won't happen in `:advanced` builds.
16 | ;; Type hints have been liberally sprinkled.
17 | ;; https://developers.google.com/closure/compiler/docs/js-for-compiler
18 | (def ^boolean debug-enabled? "@define {boolean}" ^boolean goog/DEBUG)
19 |
20 | (defn ratom [x]
21 | (reagent.core/atom x))
22 |
23 | (defn ratom? [x]
24 | (satisfies? reagent.ratom/IReactiveAtom ^clj x))
25 |
26 | (defn deref? [x]
27 | (satisfies? IDeref x))
28 |
29 |
30 | (defn make-reaction [f]
31 | (reagent.ratom/make-reaction f))
32 |
33 | (defn add-on-dispose! [a-ratom f]
34 | (reagent.ratom/add-on-dispose! a-ratom f))
35 |
36 | (defn dispose! [a-ratom]
37 | (reagent.ratom/dispose! a-ratom))
38 |
39 | (defn set-timeout! [f ms]
40 | (js/setTimeout f ms))
41 |
42 | (defn now []
43 | (if (and
44 | (exists? js/performance)
45 | (exists? js/performance.now))
46 | (js/performance.now)
47 | (js/Date.now)))
48 |
49 | (defn reagent-id
50 | "Produces an id for reactive Reagent values
51 | e.g. reactions, ratoms, cursors."
52 | [reactive-val]
53 | (when (implements? reagent.ratom/IReactiveAtom ^clj reactive-val)
54 | (str (condp instance? reactive-val
55 | reagent.ratom/RAtom "ra"
56 | reagent.ratom/RCursor "rc"
57 | reagent.ratom/Reaction "rx"
58 | reagent.ratom/Track "tr"
59 | "other")
60 | (hash reactive-val))))
61 |
62 | ;; Make reagent benefit from batched updates
63 |
64 | (def ^:dynamic *in-batch?* false)
65 |
66 | (defn batch-updates [f]
67 | (react-dom/unstable_batchedUpdates
68 | (fn []
69 | (binding [*in-batch?* true]
70 | (f)))))
71 |
72 | (let [flush-queues (.bind (.-flush-queues reagent.impl.batching/render-queue)
73 | reagent.impl.batching/render-queue)]
74 | (set! (.-flush-queues reagent.impl.batching/render-queue)
75 | #(batch-updates flush-queues)))
76 |
77 | (let [queue-render reagent.impl.batching/queue-render]
78 | (set! reagent.impl.batching/queue-render (fn [^clj c]
79 | (if *in-batch?*
80 | (.forceUpdate c)
81 | (queue-render c)))))
82 |
--------------------------------------------------------------------------------
/src/re_frame/loggers.cljc:
--------------------------------------------------------------------------------
1 | (ns re-frame.loggers
2 | (:require [clojure.set :refer [difference]]
3 | #?@(:clj [[clojure.string :as str]
4 | [clojure.tools.logging :as log]])))
5 |
6 | #?(:clj (defn log [level & args]
7 | (log/log level (if (= 1 (count args))
8 | (first args)
9 | (str/join " " args)))))
10 |
11 |
12 | ;; XXX should loggers be put in the registrar ??
13 | (def ^:private loggers
14 | "Holds the current set of logging functions.
15 | By default, re-frame uses the functions provided by js/console.
16 | Use `set-loggers!` to change these defaults
17 | "
18 | (atom #?(:cljs {:log (js/console.log.bind js/console)
19 | :warn (js/console.warn.bind js/console)
20 | :error (js/console.error.bind js/console)
21 | :debug (js/console.debug.bind js/console)
22 | :group (if (.-group js/console) ;; console.group does not exist < IE 11
23 | (js/console.group.bind js/console)
24 | (js/console.log.bind js/console))
25 | :groupEnd (if (.-groupEnd js/console) ;; console.groupEnd does not exist < IE 11
26 | (js/console.groupEnd.bind js/console)
27 | #())})
28 | ;; clojure versions
29 | #?(:clj {:log (partial log :info)
30 | :warn (partial log :warn)
31 | :error (partial log :error)
32 | :debug (partial log :debug)
33 | :group (partial log :info)
34 | :groupEnd #()})))
35 |
36 | (defn console
37 | [level & args]
38 | (assert (contains? @loggers level) (str "re-frame: log called with unknown level: " level))
39 | (apply (level @loggers) args))
40 |
41 |
42 | (defn set-loggers!
43 | "Change the set (or a subset) of logging functions used by re-frame.
44 | `new-loggers` should be a map with the same keys as `loggers` (above)"
45 | [new-loggers]
46 | (assert (empty? (difference (set (keys new-loggers)) (-> @loggers keys set))) "Unknown keys in new-loggers")
47 | (swap! loggers merge new-loggers))
48 |
49 | (defn get-loggers
50 | "Get the current logging functions used by re-frame."
51 | []
52 | @loggers)
53 |
--------------------------------------------------------------------------------
/src/re_frame/registry.cljc:
--------------------------------------------------------------------------------
1 | (ns re-frame.registry
2 | "In many places, re-frame asks you to associate an `id` (keyword)
3 | with a `handler` (function). This namespace contains the
4 | central registry of such associations."
5 | (:require [re-frame.interop :refer [debug-enabled?]]
6 | [lambdaisland.glogi :as log]))
7 |
8 | (def ^:dynamic *current-frame*)
9 |
10 | ;; kinds of handlers
11 | (def kinds #{:event :fx :cofx :sub})
12 |
13 | (defprotocol IRegistry
14 | (get-handler
15 | [this kind]
16 | [this kind id]
17 | [this kind id required?])
18 |
19 | (register-handler
20 | [this kind id handler-fn])
21 |
22 | (clear-handlers
23 | [this]
24 | [this kind]
25 | [this kind id]))
26 |
27 | (defrecord Registry [kinds kind->id->handler]
28 | IRegistry
29 | (get-handler [this kind]
30 | (get @kind->id->handler kind))
31 | (get-handler [this kind id]
32 | (get-in @kind->id->handler [kind id]))
33 | (get-handler [this kind id required?]
34 | (let [handler (get-handler this kind id)]
35 | (when debug-enabled? ;; This is in a separate when so Closure DCE can run
36 | (when (and required? (nil? handler)) ;; Otherwise you'd need to type hint the and with a ^boolean for DCE.
37 | (log/error :missing-handler {:kind kind :id id})))
38 | handler))
39 |
40 | (register-handler [this kind id handler-fn]
41 | (when debug-enabled? ;; This is in a separate when so Closure DCE can run
42 | (if (get-handler this kind id false)
43 | (log/trace :overwriting-handler {:kind kind :id id})
44 | (log/trace :registered-handler {:kind kind :id id}))) ;; allow it, but warn. Happens on figwheel reloads.
45 | (swap! kind->id->handler assoc-in [kind id] handler-fn)
46 | handler-fn) ;; note: returns the just registered handler
47 |
48 | (clear-handlers [this] ;; clear all kinds
49 | (reset! kind->id->handler {}))
50 | (clear-handlers [this kind] ;; clear all handlers for this kind
51 | (assert (kinds kind))
52 | (swap! kind->id->handler dissoc kind))
53 | (clear-handlers [this kind id] ;; clear a single handler for a kind
54 | (assert (kinds kind))
55 | (if (get-handler this kind id)
56 | (swap! kind->id->handler update-in [kind] dissoc id)
57 | (log/warn :msg (str "re-frame: can't clear " kind " handler for " id ". Handler not found.")))))
58 |
59 | (defn make-registry []
60 | ;; This atom contains a register of all handlers.
61 | ;; Contains a map keyed first by `kind` (of handler), and then `id`.
62 | ;; Leaf nodes are handlers.
63 | (->Registry kinds (atom {})))
64 |
--------------------------------------------------------------------------------
/src/re_frame/trace.cljc:
--------------------------------------------------------------------------------
1 | (ns re-frame.trace
2 | "Tracing for re-frame.
3 | Alpha quality, subject to change/break at any time."
4 | #?(:cljs (:require-macros [net.cgrand.macrovich :as macros]
5 | [re-frame.trace :refer [finish-trace with-trace merge-trace!]]))
6 | (:require [re-frame.interop :as interop]
7 | [re-frame.loggers :refer [console]]
8 | #?(:clj [net.cgrand.macrovich :as macros])
9 | #?(:cljs [goog.functions])))
10 |
11 | (def id (atom 0))
12 | (def ^:dynamic *current-trace* nil)
13 |
14 | (defn reset-tracing! []
15 | (reset! id 0))
16 |
17 | #?(:cljs (goog-define trace-enabled? false)
18 | :clj (def ^boolean trace-enabled? false))
19 |
20 | (defn ^boolean is-trace-enabled?
21 | "See https://groups.google.com/d/msg/clojurescript/jk43kmYiMhA/IHglVr_TPdgJ for more details"
22 | ;; We can remove this extra step of type hinting indirection once our minimum CLJS version includes
23 | ;; https://dev.clojure.org/jira/browse/CLJS-1439
24 | ;; r1.10.63 is the first version with this:
25 | ;; https://github.com/clojure/clojurescript/commit/9ec796d791b1b2bd613af2f62cdecfd25caa6482
26 | []
27 | trace-enabled?)
28 |
29 | (def trace-cbs (atom {}))
30 | (defonce traces (atom []))
31 | (defonce next-delivery (atom 0))
32 |
33 | (defn register-trace-cb
34 | "Registers a tracing callback function which will receive a collection of one or more traces.
35 | Will replace an existing callback function if it shares the same key."
36 | [key f]
37 | (if trace-enabled?
38 | (swap! trace-cbs assoc key f)
39 | (console :warn "Tracing is not enabled. Please set {\"re_frame.trace.trace_enabled_QMARK_\" true} in :closure-defines. See: https://github.com/day8/re-frame-10x#installation.")))
40 |
41 | (defn remove-trace-cb [key]
42 | (swap! trace-cbs dissoc key)
43 | nil)
44 |
45 | (defn next-id [] (swap! id inc))
46 |
47 | (defn start-trace [{:keys [operation op-type tags child-of]}]
48 | {:id (next-id)
49 | :operation operation
50 | :op-type op-type
51 | :tags tags
52 | :child-of (or child-of (:id *current-trace*))
53 | :start (interop/now)})
54 |
55 | ;; On debouncing
56 | ;;
57 | ;; We debounce delivering traces to registered cbs so that
58 | ;; we can deliver them in batches. This aids us in efficiency
59 | ;; but also importantly lets us avoid slowing down the host
60 | ;; application by running any trace code in the critical path.
61 | ;;
62 | ;; We add a lightweight check on top of goog.functions/debounce
63 | ;; to avoid constant setting and cancelling of timeouts. This
64 | ;; means that we will deliver traces between 10-50 ms from the
65 | ;; last trace being created, which still achieves our goals.
66 |
67 | (def debounce-time 50)
68 |
69 | (defn debounce [f interval]
70 | #?(:cljs (goog.functions/debounce f interval)
71 | :clj (f)))
72 |
73 | (def schedule-debounce
74 | (debounce
75 | (fn tracing-cb-debounced []
76 | (doseq [[k cb] @trace-cbs]
77 | (try (cb @traces)
78 | #?(:clj (catch Exception e
79 | (console :error "Error thrown from trace cb" k "while storing" @traces e)))
80 | #?(:cljs (catch :default e
81 | (console :error "Error thrown from trace cb" k "while storing" @traces e)))))
82 | (reset! traces []))
83 | debounce-time))
84 |
85 | (defn run-tracing-callbacks! [now]
86 | ;; Optimised debounce, we only re-debounce
87 | ;; if we are close to delivery time
88 | ;; to avoid constant setting and cancelling
89 | ;; timeouts.
90 |
91 | ;; If we are within 25 ms of next delivery
92 | (when (< (- @next-delivery 25) now)
93 | (schedule-debounce)
94 | ;; The next-delivery time is not perfectly accurate
95 | ;; as scheduling the debounce takes some time, but
96 | ;; it's good enough for our purposes here.
97 | (reset! next-delivery (+ now debounce-time))))
98 |
99 | (macros/deftime
100 | (defmacro finish-trace [trace]
101 | `(when (is-trace-enabled?)
102 | (let [end# (interop/now)
103 | duration# (- end# (:start ~trace))]
104 | (swap! traces conj (assoc ~trace
105 | :duration duration#
106 | :end (interop/now)))
107 | (run-tracing-callbacks! end#))))
108 |
109 | (defmacro with-trace
110 | "Create a trace inside the scope of the with-trace macro
111 |
112 | Common keys for trace-opts
113 | :op-type - what kind of operation is this? e.g. :sub/create, :render.
114 | :operation - identifier for the operation, for a subscription it would be the subscription keyword
115 | :tags - a map of arbitrary kv pairs"
116 | [{:keys [operation op-type tags child-of] :as trace-opts} & body]
117 | `(if (is-trace-enabled?)
118 | (binding [*current-trace* (start-trace ~trace-opts)]
119 | (try ~@body
120 | (finally (finish-trace *current-trace*))))
121 | (do ~@body)))
122 |
123 | (defmacro merge-trace! [m]
124 | ;; Overwrite keys in tags, and all top level keys.
125 | `(when (is-trace-enabled?)
126 | (let [new-trace# (-> (update *current-trace* :tags merge (:tags ~m))
127 | (merge (dissoc ~m :tags)))]
128 | (set! *current-trace* new-trace#))
129 | nil)))
130 |
--------------------------------------------------------------------------------
/src/re_frame/utils.cljc:
--------------------------------------------------------------------------------
1 | (ns re-frame.utils
2 | (:require [re-frame.loggers :refer [console]]))
3 |
4 | (defn dissoc-in
5 | "Dissociates an entry from a nested associative structure returning a new
6 | nested structure. keys is a sequence of keys. Any empty maps that result
7 | will not be present in the new structure.
8 | The key thing is that 'm' remains identical? to istelf if the path was never present"
9 | [m [k & ks :as keys]]
10 | (if ks
11 | (if-let [nextmap (get m k)]
12 | (let [newmap (dissoc-in nextmap ks)]
13 | (if (seq newmap)
14 | (assoc m k newmap)
15 | (dissoc m k)))
16 | m)
17 | (dissoc m k)))
18 |
19 | (defn first-in-vector
20 | [v]
21 | (if (vector? v)
22 | (first v)
23 | (console :error "re-frame: expected a vector, but got:" v)))
24 |
--------------------------------------------------------------------------------
/test/re_frame/fx_test.cljs:
--------------------------------------------------------------------------------
1 | (ns re-frame.fx-test
2 | (:require
3 | [cljs.test :refer-macros [is deftest async use-fixtures]]
4 | [re-frame.core :as re-frame]
5 | [re-frame.fx]
6 | [re-frame.interop :refer [set-timeout!]]
7 | [re-frame.loggers :as log]
8 | [clojure.string :as str]))
9 |
10 | ;; ---- FIXTURES ---------------------------------------------------------------
11 |
12 | ;; This fixture uses the re-frame.core/make-restore-fn to checkpoint and reset
13 | ;; to cleanup any dynamically registered handlers from our tests.
14 | (defn fixture-re-frame
15 | []
16 | (let [restore-re-frame (atom nil)]
17 | {:before #(reset! restore-re-frame (re-frame.core/make-restore-fn))
18 | :after #(@restore-re-frame)}))
19 |
20 | (use-fixtures :each (fixture-re-frame))
21 |
22 | ;; ---- TESTS ------------------------------------------------------------------
23 |
24 | (deftest dispatch-later
25 | (let [seen-events (atom [])]
26 | ;; Setup and exercise effects handler with :dispatch-later.
27 | (re-frame/reg-event-fx
28 | ::later-test
29 | (fn [_world _event-v]
30 | (re-frame/reg-event-db
31 | ::watcher
32 | (fn [db [_ token]]
33 | (is (#{:event1 :event2 :event3} token) "unexpected: token passed through")
34 | (swap! seen-events #(conj % token))
35 | db))
36 | {:dispatch-later [{:ms 100 :dispatch [::watcher :event1]}
37 | {:ms 200 :dispatch [::watcher :event2]}
38 | {:ms 200 :dispatch [::watcher :event3]}]}))
39 |
40 | (async done
41 | (set-timeout!
42 | (fn []
43 | (is (= @seen-events [:event1 :event2 :event3]) "All 3 events should have fired in order")
44 | (done))
45 | 1000)
46 | ;; kick off main handler
47 | (re-frame/dispatch [::later-test]))))
48 |
49 | (re-frame/reg-event-fx
50 | ::missing-handler-test
51 | (fn [_world _event-v]
52 | {:fx-not-exist [:nothing :here]}))
53 |
54 | (deftest report-missing-handler
55 | (let [logs (atom [])
56 | log-fn (fn [& args] (swap! logs conj (str/join args)))
57 | original-loggers (log/get-loggers)]
58 | (try
59 | (log/set-loggers! {:error log-fn})
60 | (re-frame/dispatch-sync [::missing-handler-test])
61 | (is (re-matches #"re-frame: no handler registered for effect::fx-not-exist. Ignoring." (first @logs)))
62 | (is (= (count @logs) 1))
63 | (finally
64 | (log/set-loggers! original-loggers)))))
65 |
--------------------------------------------------------------------------------
/test/re_frame/restore_test.cljs:
--------------------------------------------------------------------------------
1 | (ns re-frame.restore-test
2 | (:require [cljs.test :refer-macros [is deftest async use-fixtures testing]]
3 | [re-frame.core :as core :refer [make-restore-fn reg-sub subscribe]]
4 | [re-frame.subs :as subs]))
5 |
6 | ;; TODO: future tests in this area could check DB state and registrations are being correctly restored.
7 |
8 | (use-fixtures :each {:before (partial subs/clear-all-handlers! core/default-frame)})
9 |
10 | (defn one? [x] (= 1 x))
11 | (defn two? [x] (= 2 x))
12 |
13 | (defn register-test-subs []
14 | (reg-sub
15 | :test-sub
16 | (fn [db ev]
17 | (:test-sub db)))
18 |
19 | (reg-sub
20 | :test-sub2
21 | (fn [db ev]
22 | (:test-sub2 db))))
23 |
24 | (deftest make-restore-fn-test
25 | (testing "no existing subs, then making one subscription"
26 | (register-test-subs)
27 | (let [original-subs @(:subs-cache core/default-frame)
28 | restore-fn (make-restore-fn)]
29 | (is (zero? (count original-subs)))
30 | @(subscribe [:test-sub])
31 | (is (one? (count @(:subs-cache core/default-frame))))
32 | (is (contains? @(:subs-cache core/default-frame) [[:test-sub] []]))
33 | (restore-fn)
34 | (is (zero? (count @(:subs-cache core/default-frame)))))))
35 |
36 | (deftest make-restore-fn-test2
37 | (testing "existing subs, making more subscriptions"
38 | (register-test-subs)
39 | @(subscribe [:test-sub])
40 | (let [original-subs @(:subs-cache core/default-frame)
41 | restore-fn (make-restore-fn)]
42 | (is (one? (count original-subs)))
43 | @(subscribe [:test-sub2])
44 | (is (contains? @(:subs-cache core/default-frame) [[:test-sub2] []]))
45 | (is (two? (count @(:subs-cache core/default-frame))))
46 | (restore-fn)
47 | (is (not (contains? @(:subs-cache core/default-frame) [[:test-sub2] []])))
48 | (is (one? (count @(:subs-cache core/default-frame)))))))
49 |
50 | (deftest make-restore-fn-test3
51 | (testing "existing subs, making more subscriptions with different params on same subscriptions"
52 | (register-test-subs)
53 | @(subscribe [:test-sub])
54 | (let [original-subs @(:subs-cache core/default-frame)
55 | restore-fn (make-restore-fn)]
56 | (is (one? (count original-subs)))
57 | @(subscribe [:test-sub :extra :params])
58 | (is (two? (count @(:subs-cache core/default-frame))))
59 | (restore-fn)
60 | (is (one? (count @(:subs-cache core/default-frame)))))))
61 |
62 | (deftest nested-restores
63 | (testing "running nested restores"
64 | (register-test-subs)
65 | (let [restore-fn-1 (make-restore-fn)
66 | _ @(subscribe [:test-sub])
67 | _ (is (one? (count @(:subs-cache core/default-frame))))
68 | restore-fn-2 (make-restore-fn)]
69 | @(subscribe [:test-sub2])
70 | (is (two? (count @(:subs-cache core/default-frame))))
71 | (restore-fn-2)
72 | (is (one? (count @(:subs-cache core/default-frame))))
73 | (restore-fn-1)
74 | (is (zero? (count @(:subs-cache core/default-frame)))))))
75 |
--------------------------------------------------------------------------------
/test/re_frame/router_test.clj:
--------------------------------------------------------------------------------
1 | (ns re-frame.router-test
2 | (:require [clojure.test :refer :all]
3 | [re-frame.frame :as frame]))
4 |
5 | (def frame (atom nil))
6 |
7 | (defn init-frame []
8 | (reset! frame (doto (frame/make-frame)
9 | (frame/reg-event-db ::test
10 | (fn [db [_ i]]
11 | (update db ::test (fnil conj []) i)))
12 |
13 | (frame/reg-fx ::promise
14 | (fn [{:keys [p val]}]
15 | (deliver p val)))
16 |
17 | (frame/reg-event-fx ::sentinel
18 | (fn [cofx [_ p val]]
19 | {::promise {:p p :val val}})))))
20 |
21 | (use-fixtures :each {:before init-frame})
22 |
23 | (deftest dispatching-race-condition-469-test
24 | ;; Checks for day8/re-frame#469
25 | (let [p (promise)]
26 | (is (nil? (dotimes [i 1000]
27 | (frame/dispatch @frame [::test i]))))
28 | (is (nil? (frame/dispatch @frame [::sentinel p ::done])))
29 | (let [val (deref p 1000 ::timed-out)]
30 | (is (= ::done val)))
31 | (is (= (::test @(:app-db @frame))
32 | (range 1000)))))
33 |
--------------------------------------------------------------------------------
/test/re_frame/test_runner.cljs:
--------------------------------------------------------------------------------
1 | (ns re-frame.test-runner
2 | (:refer-clojure :exclude (set-print-fn!))
3 | (:require
4 | [cljs.test :as cljs-test :include-macros true]
5 | [jx.reporter.karma :as karma :include-macros true]
6 | ;; Test Namespaces -------------------------------
7 | [re-frame.interceptor-test]
8 | [re-frame.subs-test]
9 | [re-frame.fx-test]
10 | [re-frame.trace-test]
11 | [re-frame.restore-test]))
12 |
13 | (enable-console-print!)
14 |
15 | ;; ---- BROWSER based tests ----------------------------------------------------
16 | (defn ^:export set-print-fn! [f]
17 | (set! cljs.core.*print-fn* f))
18 |
19 |
20 | (defn ^:export run-html-tests []
21 | (cljs-test/run-tests
22 | 're-frame.interceptor-test
23 | 're-frame.subs-test
24 | 're-frame.fx-test
25 | 're-frame.trace-test
26 | 're-frame.restore-test))
27 |
28 | ;; ---- KARMA -----------------------------------------------------------------
29 |
30 | (defn ^:export run-karma [karma]
31 | (karma/run-tests
32 | karma
33 | 're-frame.interceptor-test
34 | 're-frame.subs-test
35 | 're-frame.fx-test
36 | 're-frame.trace-test
37 | 're-frame.restore-test))
38 |
--------------------------------------------------------------------------------
/test/re_frame/trace_test.cljs:
--------------------------------------------------------------------------------
1 | (ns re-frame.trace-test
2 | (:require [cljs.test :as test :refer-macros [is deftest]]
3 | [re-frame.trace :as trace :include-macros true]
4 | [re-frame.core :as rf]))
5 |
6 | (def test-traces (atom []))
7 |
8 | (test/use-fixtures :once {:before (fn []
9 | (trace/register-trace-cb :test
10 | (fn [traces]
11 | (doseq [trace traces]
12 | (swap! test-traces conj trace)))))
13 | :after (fn []
14 | (trace/remove-trace-cb :test))})
15 |
16 | (test/use-fixtures :each {:before (fn []
17 | (reset! test-traces [])
18 | (trace/reset-tracing!))})
19 |
20 | ; Disabled, as goog-define doesn't work in optimizations :whitespace
21 | ;(deftest trace-cb-test
22 | ; (trace/with-trace {:operation :test1
23 | ; :op-type :test})
24 | ; (is (= 1 (count @test-traces)))
25 | ; (is (= (select-keys (first @test-traces) [:id :operation :op-type :tags])
26 | ; {:id 1 :operation :test1 :op-type :test :tags nil})))
27 | ;
28 | ;(enable-console-print!)
29 | ;
30 | ;(deftest sub-trace-test
31 | ; (rf/subscribe [:non-existence])
32 | ; (is (= 1 (count @test-traces)))
33 | ; (is (= (select-keys (first @test-traces) [:id :operation :op-type :error])
34 | ; {:id 1 :op-type :sub/create :operation :non-existence :error true})))
35 |
--------------------------------------------------------------------------------
/test/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | re-frame Unit Tests
4 |
5 |
6 |
7 |
8 |
9 |
36 |
37 |
38 |
39 |