20 |
21 |
--------------------------------------------------------------------------------
/CITATION.md:
--------------------------------------------------------------------------------
1 | To cite re-frame in publications, please use:
2 |
3 | Thompson, M. (2015, March). Re-Frame: A Reagent Framework For Writing SPAs, in Clojurescript.
4 | Zenodo. http://doi.org/10.5281/zenodo.xxxxx
5 |
6 | @misc{thompson_2015,
7 | author = {Thompson, Michael},
8 | title = {Re-Frame: A Reagent Framework For Writing SPAs, in Clojurescript.},
9 | month = mar,
10 | year = 2015,
11 | doi = {10.5281/zenodo.xxxx},
12 | url = {https://doi.org/10.5281/zenodo.xxxxx}
13 | }
14 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to re-frame
2 |
3 | Thank you for taking the time to contribute! Please note that re-frame follows a [code of conduct](docs/Code-Of-Conduct.md).
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 develop branch**, work will be merged onto master when it is ready to be released.
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | dependencies:
2 | pre:
3 | - npm install karma-cli -g
4 | cache_directories:
5 | - node_modules
6 | test:
7 | override:
8 | - lein karma-once
9 | - karma start --single-run --reporters junit,dots
10 |
--------------------------------------------------------------------------------
/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/Basic-App-Structure.md:
--------------------------------------------------------------------------------
1 |
2 | ## Simpler Apps
3 |
4 | To build a re-frame app, you:
5 | - design your app's data structures (data layer)
6 | - write Reagent view functions (domino 5)
7 | - write event handler functions (control layer and/or state transition layer, domino 2)
8 | - write subscription functions (query layer, domino 4)
9 |
10 | For simpler apps, you should put code for each layer into separate files:
11 | ```
12 | src
13 | ├── core.cljs <--- entry point, plus history, routing, etc
14 | ├── db.cljs <--- schema, validation, etc (data layer)
15 | ├── views.cljs <--- reagent views (view layer)
16 | ├── events.cljs <--- event handlers (control/update layer)
17 | └── subs.cljs <--- subscription handlers (query layer)
18 | ```
19 |
20 | For a living example of this approach, look at the [todomvc example](https://github.com/Day8/re-frame/tree/master/examples/todomvc).
21 |
22 | *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.*
23 |
24 | ### There's A Small Gotcha
25 |
26 | If you adopt this structure, there's a gotcha.
27 |
28 | `events.cljs` and `subs.cljs` will never be `required` by any other
29 | namespaces. To the Google Closure dependency mechanism it appears as
30 | if these two namespaces are not needed and it doesn't load them.
31 |
32 | And, if the code does not get loaded, the registrations in these namespaces
33 | never happen. You'll then you'll be puzzled as to why none of your events handlers
34 | are registered.
35 |
36 | Once you twig to what's going on, the solution is easy. You must
37 | explicitly `require` both namespaces, `events` and `subs`, in your `core`
38 | namespace. Then they'll be loaded and the registrations will occur
39 | as that loading happens.
40 |
41 | ## Larger Apps
42 |
43 | Assuming your larger apps have multiple "panels" (or "views") which are
44 | relatively independent, you might use this structure:
45 | ```
46 | src
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 | ***
63 |
64 | Previous: [Correcting a wrong](SubscriptionsCleanup.md)
65 | Up: [Index](README.md)
66 | Next: [Navigation](Navigation.md)
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/docs/Code-Of-Conduct.md:
--------------------------------------------------------------------------------
1 | # Open Source Code of Conduct
2 |
3 | In order to foster an inclusive, kind, harassment-free, and cooperative community, Day8 enforces this code of conduct on our open source projects.
4 |
5 | ## Summary
6 |
7 | Harassment in code and discussion or violation of physical boundaries is completely unacceptable anywhere in Day8’s project codebases, issue trackers, chatrooms, mailing lists, meetups, and other events. Violators will be warned by the core team. Repeat violations will result in being blocked or banned by the core team at or before the 3rd violation.
8 |
9 | ## In detail
10 |
11 | Harassment includes offensive verbal comments related to gender identity, gender expression, sexual orientation, disability, physical appearance, body size, race, religion, sexual images, deliberate intimidation, stalking, sustained disruption, and unwelcome sexual attention.
12 |
13 | Individuals asked to stop any harassing behavior are expected to comply immediately.
14 |
15 | Maintainers are also subject to the anti-harassment policy.
16 |
17 | If anyone engages in harassing behavior, including maintainers, we may take appropriate action, up to and including warning the offender, deletion of comments, removal from the project’s codebase and communication systems, and escalation to GitHub support.
18 |
19 | If you are being harassed, notice that someone else is being harassed, or have any other concerns, please contact a member of the core team or email conduct@day8.com.au immediately.
20 |
21 | We expect everyone to follow these rules anywhere in Day8's project codebases, issue trackers, chatrooms, and mailing lists.
22 |
23 | Finally, don't forget that it is human to make mistakes! We all do. Let’s work together to help each other, resolve issues, and learn from the mistakes that we will all inevitably make from time to time.
24 |
25 | ## Thanks
26 | Thanks to the [Thoughtbot Code of Conduct][thoughtbot], [CocoaPods Code of Conduct][cocoapods], [Bundler Code of Conduct][bundler], [JSConf Code of Conduct][jsconf], and [Contributor Covenant][contributor] for inspiration and ideas.
27 |
28 | ## License
29 | To the extent possible under law, the Day8 team has waived all copyright and related or neighboring rights to Day8 Code of Conduct. This work is published from Australia.
30 |
31 |
32 |
33 | [thoughtbot]: https://thoughtbot.com/open-source-code-of-conduct
34 | [cocoapods]: https://github.com/CocoaPods/CocoaPods/blob/master/CODE_OF_CONDUCT.md
35 | [bundler]: http://bundler.io/conduct.html
36 | [jsconf]: http://jsconf.com/codeofconduct.html
37 | [contributor]: http://contributor-covenant.org/
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/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: [Mental Model Omnibus](MentalModelOmnibus.md)
16 | Up: [Index](README.md)
17 | Next: [Effectful Handlers](EffectfulHandlers.md)
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/docs/External-Resources.md:
--------------------------------------------------------------------------------
1 | ## External Resources
2 |
3 | Please add to this list by submitting a pull request.
4 |
5 |
6 | ### Templates
7 |
8 | * [re-frame-template](https://github.com/Day8/re-frame-template) - Generates the client side SPA
9 |
10 | * [Luminus](http://www.luminusweb.net) - Generates SPA plus server side.
11 |
12 | * [re-natal](https://github.com/drapanjanas/re-natal) - React Native apps.
13 |
14 | * [Slush-reframe](https://github.com/kristianmandrup/slush-reframe) - A scaffolding generator for re-frame run using NodeJS. Should work wih re-frame `0.7.0` if used on a project started from the `0.7.0` version of re-frame-template.
15 |
16 | * [Celibidache](https://github.com/velveteer/celibidache/) - An opinionated starter for re-frame applications using Boot. Based on re-frame `0.7.0`
17 |
18 |
19 | ### Examples and Applications Using re-frame
20 |
21 | * [How to create decentralised apps with re-frame and Ethereum](https://medium.com/@matus.lestan/how-to-create-decentralised-apps-with-clojurescript-re-frame-and-ethereum-81de24d72ff5#.b9xh9xnis) - Tutorial with links to code and live example.
22 |
23 | * [Elfeed-cljsrn](https://github.com/areina/elfeed-cljsrn) - A mobile client for [Elfeed](https://github.com/skeeto/elfeed) rss reader, built with React Native.
24 |
25 | * [Memory Hole](https://github.com/yogthos/memory-hole) - A small issue tracking app written with Luminus and re-frame.
26 |
27 | * [Crossed](https://github.com/velveteer/crossed/) - A multiplayer crossword puzzle generator. Based on re-frame `0.7.0`
28 |
29 | * [imperimetric](https://github.com/Dexterminator/imperimetric) - Webapp for converting texts with some system of measurement to another, such as imperial to metric.
30 |
31 | * [Brave Clojure Open Source](https://github.com/braveclojure/open-source) A site using re-frame, liberator, boot and more to display active github projects that powers [http://open-source.braveclojure.com](http://open-source.braveclojure.com). Based on re-frame `0.6.0`
32 |
33 | * [flux-challenge with re-frame](https://github.com/staltz/flux-challenge/tree/master/submissions/jelz) - flux-challenge is "a frontend challenge to test UI architectures and solutions". This is a ClojureScript + re-frame version. Based on re-frame `0.5.0`
34 |
35 | * [fractalify](https://github.com/madvas/fractalify/) -
36 | An entertainment and educational webapp for creating & sharing fractal images that powers [fractalify.com](http://fractalify.com). Based on re-frame `0.4.1`
37 |
38 | * [Angular Phonecat tutorial in re-frame](http://dhruvp.github.io/2015/03/07/re-frame/) - A detailed step-by-step tutorial that ports the Angular Phonecat tutorial to re-frame. Based on re-frame `0.2.0`
39 |
40 | * [Braid](https://github.com/braidchat/braid) - A new approach to group chat, designed around conversations and tags instead of rooms.
41 |
42 | ### Effect and CoEffect Handlers
43 |
44 | * [async-flow-fx](https://github.com/Day8/re-frame-async-flow-fx) - manage a boot process dominated by async
45 | * [http-fx](https://github.com/Day8/re-frame-http-fx) - performing Ajax tasks (via cljs-ajax)
46 | * [re-frame-forward-events-fx](https://github.com/Day8/re-frame-forward-events-fx) - slightly exotic
47 | * [cookie-fx](https://github.com/SMX-LTD/re-frame-cookie-fx) - set and get cookies
48 | * [document-fx](https://github.com/SMX-LTD/re-frame-document-fx) - set and get on `js/document` attributes
49 | * [re-frame-youtube-fx](https://github.com/micmarsh/re-frame-youtube-fx) - YouTube iframe API wrapper
50 | * [re-frame-web3-fx](https://github.com/madvas/re-frame-web3-fx) - Ethereum Web3 API
51 | * [re-frame-google-analytics-fx](https://github.com/madvas/re-frame-google-analytics-fx) - Google Analytics API
52 |
53 | ### Routing
54 |
55 | * (Bidirectional using Silk and Pushy)[https://pupeno.com/2015/08/18/no-hashes-bidirectional-routing-in-re-frame-with-silk-and-pushy/]
56 |
57 | ### Tools, Techniques & Libraries
58 |
59 | * [re-frame-undo](https://github.com/Day8/re-frame-undo) - An undo library for re-frame
60 | * Animation using `react-flip-move` - http://www.upgradingdave.com/blog/posts/2016-12-17-permutation.html
61 | * [re-frisk](https://github.com/flexsurfer/re-frisk) - A library for visualizing re-frame data and events.
62 | * [re-thread](https://github.com/yetanalytics/re-thread) - A library for running re-frame applications in Web Workers.
63 | * [re-frame-datatable](https://github.com/kishanov/re-frame-datatable) - DataTable UI component built for use with re-frame.
64 | * [Stately: State Machines](https://github.com/nodename/stately) also https://www.youtube.com/watch?v=klqorRUPluw
65 | * [re-frame-test](https://github.com/Day8/re-frame-test) - Integration Testing (not documented)
66 | * [re-learn](https://github.com/oliyh/re-learn) - Data driven tutorials for educating users of your reagent / re-frame app, built with re-frame
67 |
68 | ### Videos
69 |
70 | * [re-frame your ClojureScript applications](https://youtu.be/cDzjlx6otCU) - re-frame presentation given at Clojure/Conj 2016
71 |
72 | * [A Video Tour of the Source Code of Ninja Tools](https://carouselapps.com/2015/12/02/tour-of-the-source-code-of-ninja-tools/)
73 |
74 | ### Server Side Rendering
75 |
76 | * [Prerenderer](https://github.com/pupeno/prerenderer) - Server pre-rendering library using NodeJS that works with re-frame `0.6.0` (later versions untested)
77 | Rationale [Part 1](https://carouselapps.com/2015/09/14/isomorphic-clojurescriptjavascript-for-pre-rendering-single-page-applications-part-2/)
78 | [Part 2](https://carouselapps.com/2015/09/14/isomorphic-clojurescriptjavascript-for-pre-rendering-single-page-applications-part-2/)
79 | [Part 3](https://pupeno.com/2015/10/02/isomorphic-javascript-with-clojurescript-for-pre-rendering-single-page-applications-part-3/)
80 | [Release Announcement](https://pupeno.com/2015/12/13/prerenderer-0-2-0-released/)
81 |
82 | * [Server Side Rendering with re-frame](http://davidtanzer.net/server_side_rendering_with_re_frame) - Blog post on rendering re-frame views with Clojure.
83 |
84 | * [Rendering Reagent on the Server Using Hiccup](http://yogthos.net/posts/2015-11-24-Serverside-Reagent.html)- Blog post on rendering Reagent with Clojure.
85 |
86 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/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/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 | You are [using cljs-devtools](https://github.com/binaryage/cljs-devtools), right?
12 | If not, stop everything and immediately make that happen.
13 |
14 | ### Better Answer
15 |
16 | Are you sure you need to?
17 |
18 | First, you seldom want to inspect all of `app-db`.
19 | And, second, inspecting via a REPL might be clumsy.
20 |
21 | Instead, you probably want to inspect a part of `app-db`. __And__ you probably want
22 | to inspect it directly in the GUI itself, not off in a REPL.
23 |
24 | Here is a useful technique from @escherize. Add something like this to
25 | the hiccup of your view ...
26 | ```clj
27 | [:pre (with-out-str (pprint @interesting))]
28 | ```
29 | This assumes that `@interesting` is the value (ratom or subscription)
30 | you want to observe (note the @ in front).
31 |
32 | `pprint` output is nice to read, but not compact. For a more compact view, do this:
33 | ```clj
34 | [:pre (pr-str @some-atom)] ;; NB: using pr-str instead of pprint
35 | ```
36 |
37 | If you choose to use `pprint` then you'll need to `require` it within the `ns` of your `view.cljs`:
38 | ```clj
39 | [cljs.pprint :refer [pprint]]
40 | ```
41 |
42 | Finally, combining the short and long answers, you could even do this:
43 | ```clj
44 | [:pre (with-out-str (pprint @re-frame.db/app-db))] ;; see everything!
45 | ```
46 | or
47 | ```clj
48 | [:pre (with-out-str (pprint (:part @re-frame.db/app-db)))] ;; see a part of it!
49 | ```
50 |
51 | You definitely have [clj-devtools](https://github.com/binaryage/cljs-devtools) installed now, right?
52 |
53 | ### Other Inspection Tools
54 |
55 | Another very strong tool is [re-Frisk](https://github.com/flexsurfer/re-frisk) which
56 | provides a nice solution for navigating and inspecting your re-frame data structures.
57 |
58 | @yogthos' [json-html library](https://github.com/yogthos/json-html) provides
59 | a slick presentation, at the expense of more screen real estate, and the
60 | need to include specific CSS.
61 |
62 | ***
63 |
64 | Up: [FAQ Index](README.md)
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/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 | ### Answer
7 |
8 | So there's two things to say about this:
9 | - if you want to `dispatch` a js event object to a re-frame
10 | event handler, you must call `(.persist event)` before the `dispatch`.
11 | React recycles events (using a pool), and re-frame event handlers
12 | run async. [Find out more here](https://facebook.github.io/react/docs/events.html)
13 |
14 | - it is probably more idiomatic to extract the salient data from the event
15 | and `dispatch` that, rather than the js event object itself. When you
16 | `dispatch` pure, simple cljs data (ie. rather than js objects) testing
17 | and debugging will become easier.
18 |
19 |
20 | ***
21 |
22 | Up: [FAQ Index](README.md)
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/docs/FAQs/README.md:
--------------------------------------------------------------------------------
1 | ## Frequently Asked Questions
2 |
3 | 1. [How can I Inspect app-db?](Inspecting-app-db.md)
4 | 2. [How can I use a subscription in an Event Handler](UseASubscriptionInAnEventHandler.md)
5 | 2. [How do I use logging method X](Logging.md)
6 | 3. [Dispatched Events Are Null](Null-Dispatched-Events.md)
7 | 4. [Why implement re-frame in `.cljc` files](Why-CLJC.md)
8 | 5. [Why do we need to clear the subscription cache when reloading with Figwheel?](Why-Clear-Sub-Cache.md)
9 | 6. [How can I detect exceptions in Event Handlers?](CatchingEventExceptions.md)
10 |
11 |
12 |
13 | ## Want To Add An FAQ?
14 |
15 | We'd like that. Please supply a PR. Or just open an issue. Many Thanks!!
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/docs/FAQs/UseASubscriptionInAnEventHandler.md:
--------------------------------------------------------------------------------
1 | ### Question
2 |
3 | How do I access the value of a subscription from inside an event handler?
4 |
5 | ### Short Answer
6 |
7 | You shouldn't.
8 |
9 | Philosophically: subscriptions are designed to deliver "a stream" of new values
10 | over time. Within an event handler, we only need a one off value.
11 |
12 | Operationally: you'll end up with a memory leak.
13 |
14 | ### Longer Answer
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/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/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/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: [Basic App Structure](Basic-App-Structure.md)
63 | Up: [Index](README.md)
64 | Next: [Namespaced Keywords](Namespaced-Keywords.md)
65 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | ### Introduction
2 |
3 | - [This Repo's README](../README.md)
4 | - [app-db (Application State)](ApplicationState.md)
5 | - [First Code Walk-Through](CodeWalkthrough.md)
6 | - [Mental Model Omnibus](MentalModelOmnibus.md)
7 |
8 |
9 | ### Event Handlers
10 |
11 | - [Infographic Overview](EventHandlingInfographic.md)
12 | - [Effectful Handlers](EffectfulHandlers.md)
13 | - [Interceptors](Interceptors.md)
14 | - [Effects](Effects.md)
15 | - [Coeffects](Coeffects.md)
16 |
17 | ### Subscriptions
18 |
19 | - [Infographic](SubscriptionInfographic.md)
20 | - [Correcting a wrong](SubscriptionsCleanup.md)
21 | - [Flow Mechanics](SubscriptionFlow.md)
22 |
23 | ### App Structure
24 |
25 | - [Basic App Structure](Basic-App-Structure.md)
26 | - [Navigation](Navigation.md)
27 | - [Namespaced Keywords](Namespaced-Keywords.md)
28 |
29 |
30 | ### App Data
31 |
32 | - [Loading Initial Data](Loading-Initial-Data.md)
33 | - [Talking To Servers](Talking-To-Servers.md)
34 | - [Subscribing to External Data](Subscribing-To-External-Data.md)
35 |
36 |
37 | ### Debugging And Testing
38 |
39 | - [Debugging Event Handlers](Debugging-Event-Handlers.md)
40 | - [Debugging](Debugging.md)
41 | - [Testing](Testing.md)
42 |
43 |
44 | ### Miscellaneous
45 | - [FAQs](FAQs/README.md)
46 | - [External Resources](External-Resources.md)
47 | - [Eek! Performance Problems](Performance-Problems.md)
48 | - [Solve the CPU hog problem](Solve-the-CPU-hog-problem.md)
49 | - [Using Stateful JS Components](Using-Stateful-JS-Components.md)
50 | - [The re-frame Logo](The-re-frame-logo.md)
51 | - [Code Of Conduct](Code-Of-Conduct.md)
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/docs/SubscriptionInfographic.md:
--------------------------------------------------------------------------------
1 | ## Subscription Infographic
2 |
3 | There's two things to do here.
4 |
5 | **First**, please read through the
6 | annotated subscription code [in the todomvc example](https://github.com/Day8/re-frame/blob/master/examples/todomvc/src/todomvc/subs.cljs).
7 |
8 | **Then**, look at this Infographic:
9 |
10 |
11 |
12 | ***
13 |
14 | Previous: [Coeffects](Coeffects.md)
15 | Up: [Index](README.md)
16 | Next: [Correcting a wrong](SubscriptionsCleanup.md)
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/docs/SubscriptionsCleanup.md:
--------------------------------------------------------------------------------
1 | ## Subscriptions Cleanup
2 |
3 | There's a problem and we need to fix it.
4 |
5 |
6 | ### The Problem
7 |
8 | The simple example, used in the earlier code walk through, is not idomatic re-frame. It has a flaw.
9 |
10 | It does not obey the re-frame rule: **keep views as simple as possible**.
11 |
12 | A view shouldn't do any computation on input data. Its job is just to compute hiccup.
13 | The subscriptions it uses should deliver the data already in the right
14 | structure, ready for use in hiccup generation.
15 |
16 | ### Just Look
17 |
18 | Here be the horrors:
19 | ```clj
20 | (defn clock
21 | []
22 | [:div.example-clock
23 | {:style {:color @(rf/subscribe [:time-color])}}
24 | (-> @(rf/subscribe [:time])
25 | .toTimeString
26 | (clojure.string/split " ")
27 | first)])
28 | ```
29 |
30 | That view obtains data from a `[:time]` subscription and then it
31 | massages that data into the form it needs for use in the hiccup. We don't like that.
32 |
33 | ### The Solution
34 |
35 | Instead, we want to use a new `[:time-str]` subscription which will deliver the data all ready to go, so
36 | the view is 100% concerned with hiccup generation only. Like this:
37 | ```clj
38 | (defn clock
39 | []
40 | [:div.example-clock
41 | {:style {:color @(rf/subscribe [:time-color])}}
42 | @(rf/subscribe [:time-str])])
43 | ```
44 |
45 | Which, in turn, means we must write this `time-str` subscription handler:
46 | ```clj
47 | (reg-sub
48 | :time-str
49 | (fn [_ _]
50 | (subscribe [:time]))
51 | (fn [t _]
52 | (-> t
53 | .toTimeString
54 | (clojure.string/split " ")
55 | first)))
56 | ```
57 |
58 | Much better.
59 |
60 | You'll notice this new subscription handler belongs to the "Level 3"
61 | layer of the reactive flow. See the [Infographic](SubscriptionInfographic.md).
62 |
63 | ### Another Technique
64 |
65 | Above, I suggested this:
66 | ```clj
67 | (defn clock
68 | []
69 | [:div.example-clock
70 | {:style {:color @(rf/subscribe [:time-color])}}
71 | @(rf/subscribe [:time-str])])
72 | ```
73 |
74 | But that may offend your aesthetics. Too much noise with those `@`?
75 |
76 | To clean this up, we can define a new `listen` function:
77 | ```clj
78 | (defn listen
79 | [query-v]
80 | @(rf/subscribe query-v))
81 | ```
82 |
83 | And then rewrite:
84 | ```clj
85 | (defn clock
86 | []
87 | [:div.example-clock
88 | {:style {:color (listen [:time-color])}}
89 | (listen [:time-str])])
90 | ```
91 | So, at the cost of writing your own function, `listen`, the code is now less noisy
92 | AND there's less chance of us forgetting an `@` (which can lead to odd problems).
93 |
94 | ### Say It Again
95 |
96 | So, if, in code review, you saw this view function:
97 | ```clj
98 | (defn show-items
99 | []
100 | (let [sorted-items (sort @(subscribe [:items]))]
101 | (into [:div] (for [i sorted-items] [item-view i]))))
102 | ```
103 | What would you (supportively) object to?
104 |
105 | That `sort`, right? Computation in the view. Instead, we want the right data
106 | delivered to the view - its job is to simply make `hiccup`.
107 |
108 | The solution is to create a subscription that delivers items already sorted.
109 | ```clj
110 | (reg-sub
111 | :sorted-items
112 | (fn [_ _] (subscribe [:items]))
113 | (fn [items _]
114 | (sort items))
115 | ```
116 |
117 | Now, in this case the computation is a bit trivial, but the moment it is
118 | a little tricky, you'll want to test it. So separating it out from the
119 | view will make that easier.
120 |
121 | To make it testable, you may structure like this:
122 | ```clj
123 | (defn item-sorter
124 | [items _]
125 | (sort items))
126 |
127 | (reg-sub
128 | :sorted-items
129 | (fn [_ _] (subscribe [:items]))
130 | item-sorter)
131 | ```
132 |
133 | Now it is easy to test `item-sorter` independently.
134 |
135 | ### And There's Another Benefit
136 |
137 | re-frame de-duplicates signal graph nodes.
138 |
139 | If, for example, two views wanted to `(subscribe [:sorted-items])` only the one node
140 | (in the signal graph) would be created. Only one node would be doing that
141 | potentially expensive sorting operation (when items changed) and values from
142 | it would be flowing through to both views.
143 |
144 | That sort of efficiency can't happen if this views themselves are doing the `sort`.
145 |
146 |
147 | ### de-duplication
148 |
149 | As I described above, two, or more, concurrent subscriptions for the same query will source
150 | reactive updates from the one executing handler - from the one node in the signal graph.
151 |
152 | How do we know if two subscriptions are "the same"? Answer: two subscriptions
153 | are the same if their query vectors test `=` to each other.
154 |
155 | So, these two subscriptions are *not* "the same": `[:some-event 42]` `[:some-event "blah"]`. Even
156 | though they involve the same event id, `:some-event`, the query vectors do not test `=`.
157 |
158 | This feature shakes out nicely because re-frame has a data oriented design.
159 |
160 | ### A Final FAQ
161 |
162 | The following issue comes up a bit.
163 |
164 | You will end up with a bunch of level 1 `reg-sub` which
165 | look the same (they directly extract a path within `app-db`):
166 | ```clj
167 | (reg-sub
168 | :a
169 | (fn [db _]
170 | (:a db)))
171 | ```
172 |
173 | ```clj
174 | (reg-sub
175 | :b
176 | (fn [db _]
177 | (-> db :top :b)))
178 | ```
179 |
180 | Now, you think and design abstractly for a living, and that repetition will feel uncomfortable. It will
181 | call to you like a Siren: "refaaaaactoooor meeeee". "Maaaake it DRYYYY".
182 | So here's my tip: tie yourself to the mast and sail on. That repetition is good. It is serving a purpose.
183 | Just sail on.
184 |
185 | The WORST thing you can do is to flex your magnificent abstraction muscles
186 | and create something like this:
187 | ```clj
188 | (reg-sub
189 | :extract-any-path
190 | (fn [db path]
191 | (get-in db path))
192 | ```
193 |
194 | "Genius!", you think to yourself. "Now I only need one direct `reg-sub` and I supply a path to it.
195 | A read-only cursor of sorts. Look at the code I can delete."
196 |
197 | Neat and minimal it most certainly is, yes, but genius it isn't. You are now asking the
198 | code USING the subscription to provide the path. You have traded some innocuous
199 | repetition for longer term fragility, and that's not a good trade.
200 |
201 | What fragility? Well, the view which subscribes using, say, `(subscribe [:extract-any-path [:a]])`
202 | now "knows" about (depends on) the structure within `app-db`.
203 |
204 | What happens when you inevitably restructure `app-db` and put that `:a` path under
205 | another high level branch of `app-db`? You will have to run around all the views,
206 | looking for the paths supplied, knowing which to alter and which to leave alone.
207 | Fragile.
208 |
209 | We want our views to declaratively ask for data, but they should have
210 | no idea where it comes from. It is the job of a subscription to know where data comes from.
211 |
212 | Remember our rule at the top: **keep views as simple as possible**.
213 | Don't give them knowledge or tasks outside their remit.
214 |
215 |
216 | ***
217 |
218 | Previous: [Infographic](SubscriptionInfographic.md)
219 | Up: [Index](README.md)
220 | Next: [Flow Mechanics](SubscriptionFlow.md)
221 |
222 |
223 |
224 |
225 |
226 |
--------------------------------------------------------------------------------
/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/Testing.md:
--------------------------------------------------------------------------------
1 | ## Testing
2 |
3 | This is an introductory, simple exploration of testing re-frame apps. If you want some more help see [re-frame-test](https://github.com/Day8/re-frame-test)
4 |
5 | With a re-frame app, there's principally three things to test:
6 | 1. Event handlers
7 | 2. Subscription handlers
8 | 3. View functions
9 |
10 | ## Event Handlers - Part 1
11 |
12 | Event Handlers are pure functions and consequently easy to test.
13 |
14 | First, create an event handler like this:
15 | ```clj
16 | (defn my-db-handler
17 | [db v]
18 | ... return a modified version of db)
19 | ```
20 |
21 | Then, register it in a separate step:
22 | ```clj
23 | (re-frame.core/reg-event-db
24 | :some-id
25 | [some-interceptors]
26 | my-db-handler)
27 | ```
28 |
29 | With this setup, `my-db-handler` is available for direct testing.
30 |
31 | Your unittests will pass in certain values for `db` and `v`,
32 | and then ensure it returns the right (modified version of) `db`.
33 |
34 | ## Subscription Handlers
35 |
36 | Here's a subscription handler from [the todomvc example](https://github.com/Day8/re-frame/blob/master/examples/todomvc/src/todomvc/subs.cljs):
37 |
38 | ```clj
39 | (reg-sub
40 | :visible-todos
41 |
42 | ;; signal function
43 | (fn [query-v _]
44 | [(subscribe [:todos])
45 | (subscribe [:showing])])
46 |
47 | ;; computation function
48 | (fn [[todos showing] _] ;; that 1st parameter is a 2-vector of values
49 | (let [filter-fn (case showing
50 | :active (complement :done)
51 | :done :done
52 | :all identity)]
53 | (filter filter-fn todos))))
54 | ```
55 |
56 | How do we test this?
57 |
58 | We could split the computation function from its registration, like this:
59 | ```clj
60 | (defn visible-todos
61 | [[todos showing] _]
62 |
63 | (let [filter-fn (case showing
64 | :active (complement :done)
65 | :done :done
66 | :all identity)]
67 | (filter filter-fn todos)))
68 |
69 | (reg-sub
70 | :visible-todos
71 | (fn [query-v _]
72 | [(subscribe [:todos])
73 | (subscribe [:showing])])
74 | visible-todos) ;; <--- computation function used here
75 | ```
76 |
77 | That makes `visible-todos` available for direct unit testing.
78 |
79 | ## View Functions - Part 1
80 |
81 | Components/views are slightly more tricky. There's a few options.
82 |
83 | First, I have to admit an ugly secret. I don't tend to write tests for my views.
84 | Hey, don't give me that disproving frown! I have my reasons.
85 |
86 | Remember that every line of code you write is a liability. So tests have to earn
87 | their keep - they have to deliver a good cost / benefit ratio. And, in my experience
88 | with the re-frame architecture, the Reagent view components tend to be an unlikely
89 | source of bugs. There's just not much logic in them for me to get wrong.
90 |
91 | Okay, fine, don't believe me, then!!
92 |
93 | Here's how, theoretically, I'd write tests if I wasn't me ...
94 |
95 | If a Components is a [Form-1](https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components#form-1-a-simple-function)
96 | structure, then it is fairly easy to test.
97 |
98 | A trivial example:
99 | ```clj
100 | (defn greet
101 | [name]
102 | [:div "Hello " name])
103 |
104 | (greet "Wiki")
105 | ;;=> [:div "Hello " "Wiki"]
106 | ```
107 |
108 | So, here, testing involves passing values into the function and checking the data structure returned for correctness.
109 |
110 | What's returned is hiccup, of course. So how do you test hiccup for correctness?
111 |
112 | hiccup is just a clojure data structure - vectors containing keywords, and maps, and other vectors, etc.
113 | Perhaps you'd use https://github.com/nathanmarz/specter to declaratively check on the presence of certain values and structures? Or do it more manually.
114 |
115 |
116 | ## View Functions - Part 2A
117 |
118 | But what if the View Function has a subscription (via a [Form-2](https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components#form-2--a-function-returning-a-function) structure)?
119 |
120 | ```clj
121 | (defn my-view
122 | [something]
123 | (let [val (subscribe [:query-id])] <-- reactive subscription
124 | [:div .... using @val in here])))
125 | ```
126 |
127 | There's no immediately obvious way to test this as a lovely pure function. Because it is not pure.
128 |
129 | Of course, less pure ways are very possible. For example, a plan might be:
130 | 1. setup `app-db` with some values in the right places (for the subscription)
131 | 2. call `my-view` (with a parameter) which will return hiccup
132 | 3. check the hiccup structure for correctness.
133 |
134 | Continuing on, in a second phase you could then:
135 | 5. change the value in `app-db` (which will cause the subscription to fire)
136 | 6. call view functions again (hiccup returned).
137 | 7. check that the hiccup
138 |
139 | Which is all possible, if a little messy, and with one gotcha. After you change the
140 | value in `app-db` the subscription won't hold the new value straight away.
141 | It won't get calculated until the next animationFrame. And the next animationFrame
142 | won't happen until you hand back control to the browser. I think. Untested.
143 | Please report back here if you try. And you might also be able to use `reagent.core/flush` to force the view to be updated.
144 |
145 | ## View Functions - Part 2B
146 |
147 | Or ... instead of the not-very-pure method above, you could use `with-redefs` on `subscribe` to stub out re-frame altogether:
148 |
149 | ```clj
150 | (defn subscription-stub [x]
151 | (atom
152 | (case x
153 | [:query-id] 42)))
154 |
155 | (deftest some-test
156 | (with-redefs [re-frame/subscribe (subscription-stub)]
157 | (testing "some rendering"
158 | ..... somehow call or render the component and check the output)))
159 | ```
160 |
161 | For more integration level testing, you can use `with-mounted-component`
162 | from the [reagent-template](https://github.com/reagent-project/reagent-template/blob/master/src/leiningen/new/reagent/test/cljs/reagent/core_test.cljs) to render the component in the browser and validate the generated DOM.
163 |
164 | ## View Functions - Part 2C
165 |
166 | Or ... you can structure in the first place for easier testing and pure functions.
167 |
168 | The trick here is to create an outer and inner component. The outer sources the data
169 | (via a subscription), and passes it onto the inner as props (parameters).
170 |
171 | As a result, the inner component, which does the testable work, is pure and
172 | easily tested. The outer is fairly trivial.
173 |
174 | To get a more concrete idea, I'll direct you to another page on this Wiki
175 | which has nothing to do with testing, but it does use this `simple-outer-subscribe-with-complicated-inner-render`
176 | pattern for a different purpose: [[Using-Stateful-JS-Components]]
177 |
178 | Note this technique could be made simple and almost invisible via the
179 | use of macros. (Contribute one if you have it).
180 |
181 | This pattern has been independently discovered by many. For example, here
182 | it is called the [Container/Component pattern](https://medium.com/@learnreact/container-components-c0e67432e005#.mb0hzgm3l).
183 |
184 |
185 | ## Summary
186 |
187 | So, we stumbled slightly at the final hurdle with Form-2 Components. But prior
188 | to this, the testing story for re-frame was as good as it gets: you are testing
189 | a bunch of simple, pure functions. No dependency injection in sight!
190 |
191 |
192 |
193 |
194 |
195 |
--------------------------------------------------------------------------------
/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 thinks @martinklepsch simply wanted to
22 | `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. A classic case of premature abstraction and over engineering
27 | if you ask me. 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 | Attend one of their parties and you have a 50% chance of arrest.
34 |
35 | 
36 |
37 | The Functional Fundamentalists, a stern bunch, see the logo as a
38 | flowing poststructuralist rebuttal of OO's vowel duplication and
39 | horizontal adjacency. Their alternative approach, FF, is apparently
40 | fine because "everyone loves a fricative".
41 |
42 | For his part, @martinklepsch has never confirmed any theory, teasing
43 | us instead with coded clues like "Will you please stop emailing me"
44 | and "Why did you say I hit a horse?".
45 |
46 | ### Assets Where?
47 |
48 | Within this repo, look in `/images/logo/`
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/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 | ```cljs
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 | [gmap-inner @pos])))
58 | ```
59 |
60 |
61 | Notes:
62 | - `gmap-outer` obtains data via a subscription. It is quite simple - trivial almost.
63 | - 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)
64 | - 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.
65 | - when the inner component is given new props, its entire set of lifecycle functions will be engaged.
66 | - 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.
67 | - but this inner component has other lifecycle functions and this is where the real work is done.
68 | - 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.
69 | - the props passed (in this case `@pos`) in must be a map, otherwise `(reagent/props comp)` will return nil.
70 |
71 | ### Pattern Discovery
72 |
73 | This pattern has been independently discovered by many. To my knowledge,
74 | [this description of the Container/Component pattern](https://medium.com/@learnreact/container-components-c0e67432e005#.3ic1uipvu)
75 | is the first time it was written up.
76 |
77 | ### Code Credit
78 |
79 | The example gmaps code above was developed by @jhchabran in this gist:
80 | https://gist.github.com/jhchabran/e09883c3bc1b703a224d#file-2_google_map-cljs
81 |
82 | ### D3 Examples
83 |
84 | D3 (from @zachcp):
85 | - Blog Post: http://zachcp.org/blog/2015/reagent-d3/
86 | - Code: https://github.com/zachcp/simplecomponent
87 | - Example: http://zachcp.github.io/simplecomponent/
88 |
89 | A different take on using D3:
90 | https://gadfly361.github.io/gadfly-blog/2016-10-22-d3-in-reagent.html
91 |
92 | ### Advanced Lifecycle Methods
93 |
94 | If you mess around with lifecycle methods, you'll probably want to read Martin's explanations:
95 | https://www.martinklepsch.org/posts/props-children-and-component-lifecycle-in-reagent.html
96 |
97 |
98 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/docs/re-frankenstein/about-effects.md:
--------------------------------------------------------------------------------
1 | # re-frankenstein - about effects
2 |
3 | Backward compatibility with re-frame is taken really seriously in this fork. The
4 | idea was to be able to provide new features without changing too much the way
5 | re-frame is used, particularly in terms of registering handlers, effects and
6 | coeffects.
7 |
8 | That is why in the first implementation of re-frankenstein, the `do-fx`
9 | interceptor that was swapped in place of its original counterpart (on
10 | `(frank/create)`) was strictly identical to it except for the use of a local
11 | handler registry (instead of the global registry).
12 |
13 | Sadly, the result of this is that, apart from the `:db` effect, no other effect
14 | could cause affect the local db! So we are going to move just a little bit away
15 | from the original `do-fx` implementation.
16 |
17 |
18 | ## What re-frankenstein changes in 0.0.3
19 |
20 | The `do-fx` interceptor injected at the creation of a `frank` is now slightly
21 | different from in the way it invokes the effect handlers:
22 |
23 | ```
24 | ;; The original way
25 | (effect-fn value)
26 |
27 | ;; The re-frankenstein way
28 | (effect-fn value {:dispatch! #(dispatch! frank %)
29 | :dispatch-sync! #(dispatch-sync! frank %)})
30 | ```
31 |
32 | The new `do-fx` interceptor provides the effect handler a dispatch function they
33 | may call if they need to locally dispatch further. As the db and the handler
34 | registry are local when using a `frank` passing a dispatch function to the
35 | effects handler is in fact the only way for them to modify the local db of the
36 | `frank` instance.
37 |
38 | That means that when you registering an effect that you want to use locally on a
39 | `frank`, you will need to pass a function of 2 arguments:
40 |
41 | ```
42 | (reg-fx ::some-effect
43 | (fn [value {dispatch! :dispatch!}]
44 | ;; Do the side effecting here and dispatch if you need to modify state.
45 | ))
46 | ```
47 |
48 |
--------------------------------------------------------------------------------
/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/simpleexample/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, figwheel`" to compile the app and start up figwheel hot-reloading,
18 | 5. open `http://localhost:3449/example.html` to see the app
19 |
20 | While step 4 is running, any changes you make to the ClojureScript
21 | source files (in `src`) will be re-compiled and reflected in the running
22 | page immediately.
23 |
24 | ### Production Version
25 |
26 | Run "`lein do clean, with-profile prod compile`" to compile an optimised
27 | version, and then open `resources/public/example.html` in a browser.
28 |
--------------------------------------------------------------------------------
/examples/simple/project.clj:
--------------------------------------------------------------------------------
1 | (defproject simple "0.9.0"
2 | :dependencies [[org.clojure/clojure "1.8.0"]
3 | [org.clojure/clojurescript "1.9.227"]
4 | [cljsjs/react "15.4.2-0"]
5 | [cljsjs/react-dom "15.4.2-0"]
6 | [reagent "0.6.0-rc"]
7 | [org.martinklepsch/derivatives "0.2.0"]
8 | [rum "0.10.8"]
9 |
10 | [com.chpill.re-frankenstein "0.0.1-SNAPSHOT"]]
11 |
12 | :plugins [[lein-cljsbuild "1.1.3"]
13 | [lein-figwheel "0.5.4-7"]]
14 |
15 | :hooks [leiningen.cljsbuild]
16 |
17 | :profiles {:dev {:cljsbuild
18 | {:builds {:client {:figwheel {:on-jsload "simple.core/run"}
19 | :compiler {:main "simple.core"
20 | :asset-path "js"
21 | :optimizations :none
22 | :source-map true
23 | :source-map-timestamp true}}}}}
24 |
25 | :prod {:cljsbuild
26 | {:builds {:client {:compiler {:optimizations :advanced
27 | :elide-asserts true
28 | :pretty-print false}}}}}}
29 |
30 | :figwheel {:repl false}
31 |
32 | :clean-targets ^{:protect false} ["resources/public/js"]
33 |
34 | :cljsbuild {:builds {:client {:source-paths ["src"]
35 | :compiler {:output-dir "resources/public/js"
36 | :output-to "resources/public/js/client.js"}}}})
37 |
--------------------------------------------------------------------------------
/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 |