-
-
Notifications
You must be signed in to change notification settings - Fork 208
Quick Start
This is a quick introduction to Figwheel. It is intended to provide the reader with enough information to use Figwheel productively.
The aim of this document is empowerment, not bewilderment.
This Quick Start fills the same role as the ClojureScript Quick Start. Please see that Quick Start for a solid introduction to how to use the ClojureScript compiler.
Leiningen is a terrific build tool for Clojure. To use this tutorial you need to have leiningen installed on your machine. Please visit the leiningen home page to learn how to get lein running.
Create a directory to work in called hello_seymore
. Change into that directory and create a ClojureScript source file, a project file and an index.html.
mkdir hello_seymore
cd hello_seymore
touch project.clj
touch index.html
mkdir -p src/hello_seymore
touch src/hello_seymore/core.cljs
Edit src/hello_seymore/core.cljs
to look like:
(ns hello-seymore.core)
(.log js/console "Hey Seymore sup?!")
Now you will need edit the project.clj
as follows:
(defproject hello-seymore "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.7.122"]]
:plugins [[lein-figwheel "0.3.9"]]
:clean-targets [:target-path "out"]
:cljsbuild {
:builds [{:id "dev"
:source-paths ["src"]
:figwheel true
:compiler {:main "hello-seymore.core"}
}]
})
At this point make sure you are in the project root directory hello_seymore
and run Figwheel as follows:
lein figwheel
You should see a bunch of clojure libraries get downloaded and installed into your local maven repository (which happens to be in ~/.m2
on my Mac).
After that Figwheel will start up, compile your hello-seymore.core
library and try to start a repl, BUT THE REPL WILL NOT START. Sorry for the caps, but this behavior is expected.
Type Ctrl-C to quit the Figwheel process.
If you list your project directory you should see this:
$ ls
figwheel_server.log
index.html
main.js
out
project.clj
src
target
Some new files have been created. The main.js
file and the out
directory contain your compiled ClojureScript code.
If you look at the contents of main.js
you will see:
if(typeof goog == "undefined") document.write('<script src="out/goog/base.js"></script>');
document.write('<script src="out/cljs_deps.js"></script>');
document.write('<script>if (typeof goog != "undefined") { goog.require("hello_seymore.core"); } else { console.warn("ClojureScript could not load :main, did you forget to specify :asset-path?"); };</script>');
document.write("<script>if (typeof goog != \"undefined\") { goog.require(\"figwheel.connect\"); }</script>");
The last line of main.js
loads the code that will connect to the Figwheel server. This is what enables the Figwheel server to communicate with the application, which is running in the browser.
In order to run the hello Seymore program, we will need an HTML file to load the compiled program into in the browser.
Edit the index.html
file to look like this:
<!DOCTYPE html>
<html>
<head></head>
<body>
<script src="main.js" type="text/javascript"></script>
</body>
</html>
Now run Figwheel again:
$ lein figwheel
and load the index.html
in the browser from the filesystem. The location bar in your browser should have a file://<...>/hello_seymore/index.html
url in it.
Change back to the terminal where Figwheel is starting and when it finishes you should see a REPL prompt.
Go ahead and type some ClojureScript at the REPL prompt:
=> (+ 1 2 3)
6
If you get 6
as a response then you have successfully set up Figwheel!!!
You can also see that the REPL is connected to the browser:
=> (js/alert "Am I connected to Figwheel?")
nil
You should see the alert in the browser window. Only after you click on "Ok" there, will the REPL return nil
.
Also, go ahead and open up the browser's dev tools so you can see the messages from Figwheel. You should see something like this (in Chrome):
Now go back to your src/hello_seymore/core.cljs
file and change the line that looks like:
(.log js/console "Hey Seymore sup?!")
to
(.log js/console "Hey Seymore! wts goin' on?")
and save the file. You should now see Hey Seymore! wts goin' on?
printed in the dev console of your browser.
Congratulations!! You have set up Figwheel and your code is getting loaded into the browser as you save it.
As you can see, side-effects from print functions happen on every reload. This might not be desirable for all side-effects. Code that only triggers desired side-effects is called "reloadable code". We will discuss how to write such code later. Please remember that you are responsible for writing reloadable code! :)
When working with Figwheel you should really have a separate terminal open to watch the
figwheel_server.log
. This can be immensely helpful when things aren't working correctly.So open another terminal and type:
$ tail -f figwheel_server.log
Let's create a simple program for demonstration purposes. This is going to be a bare bones program that uses the sablono ClojureScript interface to React.js.
First add [sablono "0.3.5"]
to the :dependencies
list in your project.clj
.
You will need to restart Figwheel to pick up the new dependency.
Whenever you add new dependencies and restart Figwheel, also run
lein clean
. It will reduce the chances of working with old code
Add a place for us to mount our app in your index.html
.
<!DOCTYPE html>
<html>
<head></head>
<body>
<div id="app"></div> <!-- add this line -->
<script src="main.js" type="text/javascript"></script>
</body>
</html>
Now, edit your src/hello_seymore/core.cljs
file:
(ns hello-seymore.core
(:require [sablono.core :as sab]))
(def app-state (atom { :likes 0 }))
(defn like-seymore [data]
(sab/html [:div
[:h1 "Seymore's quantified popularity: " (:likes @data)]
[:div [:a {:href "#"
:onClick #(swap! data update-in [:likes] inc)}
"Thumbs up"]]]))
(defn render! []
(.render js/React
(like-seymore app-state)
(.getElementById js/document "app")))
(add-watch app-state :on-change (fn [_ _ _ _] (render!)))
(render!)
Since Figwheel is running, once you save this file you should see the application running in the browser.
Let's talk about the reloadability of this tiny React app.
First, try the program out by clicking the Thumbs up link several times. You will see Seymore's popularity increase. Popularity should really be measured by how many times people are willing to click a thumbs up eh?
Now, if you change the source files by simply adding a blank line and saving it (something I actually do pretty often), the code will reload and you will see the popularity count go to zero. This is because we are redefining the app-state
atom on every code load. This is not what we want. You can fix this by using defonce
instead of def
on app-state
as follows:
(defonce app-state (atom {:likes 0}))
After you make this change you will see that the state of the program will persist through reloads.
Something else to notice is that the call to add-watch
is getting called over and over again on reload. But this is OK because it is just replacing the current :on-change
listener, making this top level side-effect reloadable.
Also, you will notice that we have a top level call to render!
at the end of the file. This forces a re-render every time the file is loaded. This is helpful so that we will render app changes as we edit the file.
Now this top level render!
call will keep our application up to date as long as we are editing the core.cljs
file, but it will fail to help us if we edit another.
Remove the like-seymore
function from the core.cljs
file and add it to its own file src/hello_symore/components.cljs
.
Updated core.cljs
:
(ns hello-seymore.core
(:require [sablono.core :as sab]
[hello-seymore.components :refer [like-seymore]]))
(defonce app-state (atom { :likes 0 }))
(defn render! []
(.render js/React
(like-seymore app-state)
(.getElementById js/document "app")))
(add-watch app-state :on-change (fn [_ _ _ _] (render!)))
(render!)
New src/hello_seymore/components.cljs
:
(ns hello-seymore.components
(:require [sablono.core :as sab]))
(defn like-seymore [data]
(sab/html [:div
[:h1 "Seymore's quantified popularity: " (:likes @data)]
[:div [:a {:href "#"
:onClick #(swap! data update-in [:likes] inc)}
"Thumbs up"]]]))
As long as Figwheel was running this whole time your refactor should have been live loaded into the browser. After reflecting on the coolness of this, go ahead and edit the components.cljs
file and remove the word "quantified" and save it. You will notice that you won't see the change reflected in the application running in the browser.
The reason we didn't see the change reflected in the running app is because we need to have the render!
function get called again.
There are three ways to fix this. I will explore these next.
It is a common need to reinitialize an application in some way after some code has been reloaded. In the counter application we kind of cheated and placed a call to render!
at the top level so that every time edited that core.cljs
file render would get called and we would see the new changes. We will need something else to call render!
when we are editing files other than core.cljs
.
The most correct but potentially slow solution is to use :recompile-dependents true
in your build configuration as follows.
In project.clj
:
(defproject hello-seymore "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.7.122"]
[sablono "0.3.5"]]
:plugins [[lein-figwheel "0.3.9"]]
:clean-targets [:target-path "out"]
:cljsbuild {
:builds [{:id "dev"
:source-paths ["src"]
:figwheel true
:compiler {:main hello-seymore.core
:recompile-dependents true ;<-- add this
}
}]
})
Reset the autobuilder in the REPL by typing (reset-autobuild)
at the REPL prompt. This will reload this new configuration without having to restart the lein figwheel
process.
Now add and remove the word "quantified" in components.cljs
a few times and you will see that the application gets refreshed as expected.
What's happening here is that the ClojureScript compiler is recompiling the dependents and transitive dependents for the file that changed. If you look in your developer tools console you will see several files being reloaded not just the file that changed. This is useful for keeping everything up to date but as your application grows this can cause large recompiles and you will have to wait longer for your app to update. This could also make you vulnerable to certain sections of code that were not written in a reloadable manner in a large project.
So while :recompile-dependents true
works it's important to understand its downsides.
Before continuing please remove :recompile-dependents true
from the project.clj
and type (reset-autobuild)
at the REPL prompt again to reload the build configuration.
Another approach to this problem of triggering a re-render is to force the hello-seymore.core
namespace to get reloaded every time a '.cljs' file changes.
First edit components.cljs
a couple times to verify that the changes are not loading.
Then just add ^:figwheel-always
in front of the hello-seymore.core
in the core.cljs
file as so:
(ns ^:figwheel-always hello-seymore.core
(:require [sablono.core :as sab]
[hello-seymore.components :refer [like-seymore]]))
Now if you place "dubious" where "quantified" was in components.cljs
you will see the change appear in the running application.
The downsides of using ^:figwheel-always
is that the whole file is getting reloaded whenever another file gets changed and you may not want this behavior.
Now remove ^:figwheel-always
and you will notice that the previous behavior returns and behavior changes to components.cljs
do not get rendered immediately in the running app.
Probably the best overall solution is to have a specific hook that gets called after new code has been reloaded.
In this case we already have the hello-seymore.core/render!
function so let's use that. You configure the :on-jsload
hook in your project.clj
as follows:
(defproject hello-seymore "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.7.122"]
[sablono "0.3.5"]]
:plugins [[lein-figwheel "0.3.9"]]
:clean-targets [:target-path "out"]
:cljsbuild {
:builds [{:id "dev"
:source-paths ["src"]
:figwheel { :on-jsload "hello-seymore.core/render!" } ;<-- add this
:compiler { :main hello-seymore.core }
}]
})
Now reload the configuration by typing (reset-autobuild)
at the REPL prompt again, and reload your application in the browser (whenever you change a Figwheel config setting in the project.clj you will need to do reload the browser).
Now your edits to component.cljs
are displayed immediately again.
Figwheel will autoreload css as well. You will need to add some server level configuration to get this feature.
First create a CSS file css/style.css
.
body {
background-color: yellow;
}
Edit the project.clj
(defproject hello-seymore "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.7.122"]
[sablono "0.3.5"]]
:plugins [[lein-figwheel "0.3.9"]]
:clean-targets [:target-path "out"]
:cljsbuild {
:builds [{:id "dev"
:source-paths ["src"]
:figwheel { :on-jsload "hello-seymore.core/render!" }
:compiler {:main hello-seymore.core }
}]
}
:figwheel { ;; <-- add server level config here
:css-dirs ["css"]
}
)
And of course don't forget to add a link to your css in the index.html
:
<!DOCTYPE html>
<html>
<head>
<!-- add link here -->
<link href="css/style.css" rel="stylesheet" type="text/css">
</head>
<body>
<script src="main.js" type="text/javascript"></script>
</body>
</html>
Now restart Figwheel and edit the style.css
:
body {
background-color: green;
}
Your page in the browser should now be green and not yellow. Without reloading the page!
You may not want to have your code auto-reloaded but still want to benefit from Figwheel's auto-building and its REPL. All you have to do is add :autoload false
to the :figwheel
entry in your build config.
In this case your build config would look like this:
(defproject hello-seymore "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "0.0-3308"]
[sablono "0.3.5"]]
:plugins [[lein-figwheel "0.3.9"]]
:clean-targets [:target-path "out"]
:cljsbuild {
:builds [{:id "dev"
:source-paths ["src"]
:figwheel { :autoload false } ;; <<- add this
:compiler {:main hello-seymore.core }
}]
})
It is recommended that if you start needing a server to serve your ClojureScript assets that you just bite the bullet and write your own. But for convenience Figwheel has a built-in webserver. In order to use it you will have to place your assets in resources/public
.
Following the example we are using you would need to make these changes:
Move your assets into resources/public
:
mkdir -p resources/public
$ mv index.html css resources/public
Now you need to change your build config to output your compiled ClojureScript to resources/public
as well. For good measure we will place them in a cljs
directory.
(defproject hello-seymore "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "0.0-3308"]
[sablono "0.3.5"]]
:plugins [[lein-figwheel "0.3.9"]]
:clean-targets [:target-path "out" "resources/public/cljs"] ;; Add "resources/public/cljs"
:cljsbuild {
:builds [{:id "dev"
:source-paths ["src"]
:figwheel true
:compiler {:main hello-seymore.core
;; add the following
:asset-path "cljs/out"
:output-to "resources/public/cljs/main.js"
:output-dir "resources/public/cljs/out"}
}]
}
:figwheel {
:css-dirs ["resources/public/css"]
}
)
So now we have changed where the compiler is placing the compiled assets. It's very important to get :asset-path
correct otherwise main.js
will have code that doesn't point to your compiled assets and nothing will work. It is also important to add our new target path to the :clean-targets
, otherwise lein clean
won't work.
Since we changed the location of main.js
, we need to reflect that in index.html
:
<!DOCTYPE html>
<html>
<head>
<link href="css/style.css" rel="stylesheet" type="text/css">
</head>
<body>
<div id="app"></div>
<script src="cljs/main.js" type="text/javascript"></script> ;; <-- changed to "cljs/main.js"
</body>
</html>
Now you can restart Figwheel.
You can now find your application at http://localhost:3449/index.html
Since all code evaluated in the REPL actually runs on the browser, we have some powerful tools at our disposal. For example, we can take our app-state
to places our UI interface can't easily reach to test for edge cases. Imagine that there might be a problem when the counter displays a 5 digit number after a number of things happened. Would you do those things and then click 10000 times on "Thumbs up"?
You'll get a much better REPL experience if you install rlwrap and run
rlwrap lein figwheel
you can install
rlwrap
on OSX withbrew install rlwrap
Start the REPL and go to the hello-seymore.core
namespace
> (in-ns 'hello-seymore.core)
nil
Then change app-state
to whatever number you want to test and check it on the browser:
> (reset! app-state {:likes 10000})
{:likes 10000}