This is an example demonstrating Independently Connected Components pattern with clojurescript + re-frame.
Based on the original Reframe TodoMVC and React + Redux + Reselect TodoMVC Example (with ICC) example.
The example was extended to handle several lists (to show the additional comlexity). Have a look at exercises below.
There is also a slide deck from Alexey Migutsky's talk on ICC at FDConf 2017 (the slides are 2D, you can go down in some sections).
- Passing IDs through components (
main-section
) rather than whole entities - Binding components to specific handlers (
toggle-todo-checkbox
,delete-todo-button
) - Specializing components and reusing a template (
todo-input-new
andtodo-input-edit
) - Domains (todo, list and filter) and linking domains (
list-todo
,filter-list-todo
) - Render props (
todo-item
) - Dependent events using
add-post-event-callback
and:dispatch
effect.
Dependent events are tricky in re-frame as they are async, so you need to prepare for intermediate states on the view layer. A potential source of errors. Maybe there is a better way to do this, like reversing the events chain, but I would like to postpone such design desicions.
This approach provides a clean structure for components and store.
Changing the app and moving things around are much easier than with other approaches.
If you want to feel the benefits, you can fork this repo and make these exercise changes to the app:
- Rename the list to "Home".
- Add a second list called "Work". It should work independently from "Home" list.
- Make task counts work per list
- Add a third todo list called "All" containing all tasks from both "Home" and "Work" lists.
- When todo item is in "All" list, add a label to every todo item showing which list ("Home" or "Work") it belongs to.
- Extract filters (All, Active, Completed) into components and remove them from "Home" and "Work" list.
- Move a chevron (complete all tasks) to the footer of "All" list. Move completed todos counter to the header of "All" list.
- Make "Work"/"Home" and "All" lists opaque when any todo is edited in "Home"/"Work" list (you probably need a new domain?).
- Add drag'n'drop between "Work" and "Home" lists.
- Add drag'n'drop sorting inside "Work" and "Home" lists.
- Measure the performance. Try passing primitives instead of todo model in props
- Make it possible to have 10000 items in any list
- generate test data on app start
- use virtualized list
The exercises are listed by increasing complexity
- Architecture: Single Page Application (SPA)
- Languages
- Front end (re-frame): ClojureScript (CLJS)
- CSS compilation (
lein-garden
): Clojure
- Dependencies
- Build tools
- Project task & dependency management: Leiningen
- CLJS compilation, REPL, & hot reload:
shadow-cljs
- CSS compilation:
lein-garden
- Test framework: cljs.test
- Test runner: Karma
- Development tools
- Debugging: CLJS DevTools,
re-frame-10x
, re-frisk - Emacs integration: CIDER
- Linter: clj-kondo
- Debugging: CLJS DevTools,
/
: project config files.clj-kondo/
: lint config and cache files (cache files are not tracked; see.gitignore
)dev/
: source files compiled only with the dev profilecljs/user.cljs
: symbols for use during development in the ClojureScript REPL
resources/public/
: SPA root directory; dev / prod profile depends on the most recent buildindex.html
: SPA home page- Dynamic SPA content rendered in the following
div
:<div id="app"></div>
- Customizable; add headers, footers, links to other scripts and styles, etc.
- Dynamic SPA content rendered in the following
- Generated directories and files
- Created on build with either the dev or prod profile
- Deleted on
lein clean
(run by alllein
aliases before building) css/
: compiled CSS (lein-garden
, can also be compiled manually)js/compiled/
: compiled CLJS (shadow-cljs
)- Not tracked in source control; see
.gitignore
- Not tracked in source control; see
src/clj/todomvc_icc_cljs/
: CSS compilation source files (Clojure, Garden)src/cljs/todomvc_icc_cljs/
: SPA source files (ClojureScript, re-frame)core.cljs
: contains the SPA entry point,init
test/cljs/todomvc_icc_cljs/
: test files (ClojureScript, cljs.test)- Only namespaces ending in
-test
(files*_test.cljs
) are compiled and sent to the test runner
- Only namespaces ending in
Use your preferred editor or IDE that supports Clojure/ClojureScript development. See Clojure tools for some popular options.
- Install JDK 8 or later (Java Development Kit)
- Install Leiningen (Clojure/ClojureScript project task & dependency management)
- Install Node.js (JavaScript runtime environment) which should include NPM or if your Node.js installation does not include NPM also install it.
- Install karma-cli (test runner):
npm install -g karma-cli
- Install Chrome or
Chromium version 59 or later
(headless test environment)
- For Chromium, set the
CHROME_BIN
environment variable in your shell to the command that launches Chromium. For example, in Ubuntu, add the following line to your.bashrc
:export CHROME_BIN=chromium-browser
- For Chromium, set the
- Install clj-kondo (linter)
- Clone this repo and open a terminal in the
todomvc-icc-cljs
project root directory - (Optional) Download project dependencies:
lein deps
- (Optional) Setup lint cache:
clj-kondo --lint "$(lein classpath)"
- Setup linting in your editor
Browser caching should be disabled when developer tools are open to prevent interference with
shadow-cljs
hot reloading.
Custom formatters must be enabled in the browser before CLJS DevTools can display ClojureScript data in the console in a more readable way.
- Open DevTools (Linux/Windows:
F12
orCtrl-Shift-I
; macOS:⌘-Option-I
) - Open DevTools Settings (Linux/Windows:
?
orF1
; macOS:?
orFn+F1
) - Select
Preferences
in the navigation menu on the left, if it is not already selected - Under the
Network
heading, enable theDisable cache (while DevTools is open)
option - Under the
Console
heading, enable theEnable custom formatters
option
- Open Developer Tools (Linux/Windows:
F12
orCtrl-Shift-I
; macOS:⌘-Option-I
) - Open Developer Tools Settings
(Linux/macOS/Windows:
F1
) - Under the
Advanced settings
heading, enable theDisable HTTP Cache (when toolbox is open)
option
Unfortunately, Firefox does not yet support custom formatters in their devtools. For updates, follow the enhancement request in their bug tracker: 1262914 - Add support for Custom Formatters in devtools.
Start a temporary local web server, build the app with the dev
profile, and serve the app,
browser test runner and karma test runner with hot reload:
lein watch
Please be patient; it may take over 20 seconds to see any output, and over 40 seconds to complete.
When [:app] Build completed
appears in the output, browse to
http://localhost:8280/.
shadow-cljs
will automatically push ClojureScript code
changes to your browser on save. To prevent a few common issues, see
Hot Reload in ClojureScript: Things to avoid.
Opening the app in your browser starts a ClojureScript browser REPL, to which you may now connect.
Connect to the browser REPL:
M-x cider-jack-in-cljs
See
Shadow CLJS User's Guide: Emacs/CIDER
for more information. Note that the mentioned .dir-locals.el
file has already
been created for you.
See
Shadow CLJS User's Guide: Editor Integration.
Note that lein watch
runs shadow-cljs watch
for you, and that this project's running build ids is
app
, browser-test
, karma-test
, or the keywords :app
, :browser-test
, :karma-test
in a Clojure context.
Alternatively, search the web for info on connecting to a shadow-cljs
ClojureScript browser REPL
from your editor and configuration.
For example, in Vim / Neovim with fireplace.vim
- Open a
.cljs
file in the project to activatefireplace.vim
- In normal mode, execute the
Piggieback
command with this project's running build id,:app
::Piggieback :app
-
Connect to the
shadow-cljs
nREPL:lein repl :connect localhost:8777
The REPL prompt,
shadow.user=>
, indicates that is a Clojure REPL, not ClojureScript. -
In the REPL, switch the session to this project's running build id,
:app
:(shadow.cljs.devtools.api/nrepl-select :app)
The REPL prompt changes to
cljs.user=>
, indicating that this is now a ClojureScript REPL. -
See
user.cljs
for symbols that are immediately accessible in the REPL without needing torequire
.
Build the app with the prod
profile, start a temporary local web server, launch headless
Chrome/Chromium, run tests, and stop the web server:
lein ci
Please be patient; it may take over 15 seconds to see any output, and over 25 seconds to complete.
Or, for auto-reload:
lein watch
Then in another terminal:
karma start
Use Clojure and Garden to edit styles in .clj
files located
in the src/clj/todomvc_icc_cljs/
directory. CSS files are compiled
automatically on dev
or prod
build.
Manually compile CSS files:
lein garden once
The resources/public/css/
directory is created, containing the compiled CSS files.
Enable automatic compiling of CSS files when source .clj
files are changed:
lein garden auto
See a list of shadow-cljs CLI
actions:
lein run -m shadow.cljs.devtools.cli --help
Please be patient; it may take over 10 seconds to see any output. Also note that some actions shown may not actually be supported, outputting "Unknown action." when run.
Run a shadow-cljs action on this project's build id (without the colon, just app
):
lein run -m shadow.cljs.devtools.cli <action> app
The debug?
variable in config.cljs
defaults to true
in
dev
builds, and false
in prod
builds.
Use debug?
for logging or other tasks that should run only on dev
builds:
(ns todomvc-icc-cljs.example
(:require [todomvc-icc-cljs.config :as config])
(when config/debug?
(println "This message will appear in the browser console only on dev builds."))
Build the app with the prod
profile:
lein release
Please be patient; it may take over 15 seconds to see any output, and over 30 seconds to complete.
The resources/public/js/compiled
directory is created, containing the compiled app.js
and
manifest.edn
files.
The resources/public
directory contains the complete, production web front
end of your app.
Always inspect the resources/public/js/compiled
directory prior to deploying the app. Running any
lein
alias in this project after lein watch
will, at the very least, run lein clean
, which
deletes this generated directory. Further, running lein watch
will generate many, much larger
development versions of the files in this directory.