In this tutorial we are going to investigate the issue we met in the previous tutorial and try to solve it.
If you want to start working from the end of the previous tutorial, assuming you've git installed, do as follows:
git clone https://github.com/magomimmo/modern-cljs.git
cd modern-cljs
git checkout se-tutorial-05
Our latest tutorial ended with a not so nice error. We discovered
that as soon as we have two HTML pages linking the same js/main.js
generated JS file, the init
function we set for the onload
property of the JS window
object for the index.html
page was not
the one we defined in login.cljs
, but the one we defined in
shopping.cljs
.
As we anticipated in the previous tutorial, this behavior
depends on the Google Closure/CLJS pair of compilers driven by the
boot-cljs
task.
In the first tutorial, we set :source-paths
in the build.boot
file to the #{"src/cljs"}
path.
The :source-paths
directive instructs the Google Closure/CLJS pair of
compilers to look for any CLJS source code in the src/cljs
directory
structure for doing its job.
I'm not going to explain every detail of the CLJS/GCSL
compilers. The only detail that is useful for investigating and
eventually solving the above issue is that the pair of compilers
generates a single JS file (i.e., js/main.js
) from all of
the CLJS files it finds in the src/cljs
directory and subdirectories
(i.e., core.cljs
, login.cljs
, and shopping.cljs
).
Both login.cljs
and shopping.cljs
have a final call to (set! (.-onload js/window) init)
, which is therefore called twice: once
from login.cljs
and once from shopping.cljs
. The order of these
calls is critical, because whichever comes first, the other is going
to overwrite the previous value: a clear case against JS mutable data
structures?
From the above discussion the reader could infer that CLJS is
good only for Single Page Applications (SPA). Indeed, there is a very
modest solution to the above conflict between more calls setting the
same onload
property of the JS window
object: code duplication!
You have to duplicate the directory structure and the corresponding build options for each html page that is going to include the single generated JS file.
I don't know about you, but if there is something that I hate more than a WARNING notification by a compiler it is code duplication. So, I'm not even going to explain how to duplicate your code to modestly solve the above error.
Now the simple made easy way:
- remove the call
(set! (.-onload js/window) init)
from bothlogin.cljs
andshopping.cljs
files; - add the
:export
tag (metadata) to theinit
function in bothlogin.cljs
andshopping.cljs
files; - add a
script
tag calling the correpondinginit
function in bothindex.html
andshopping.html
files; - you're done.
NOTE 2: If you do not
^:export
a CLJS function, it will be subject to Google Closure Compileroptimizations
strategies. When set tosimple
oradvanced
, the GCSL compiler will minify the emitted JS file and any local variable or function name will be shortened/obfuscated and won't be available from external JS code. If a variable or function name is annotated with:export
metadata, its name will be preserved and can be called by standard JS code. In our example the two functions will be available as:modern_cljs.login.init()
andmodern_cljs.shopping.init()
.
Here is the related fragment of login.cljs
:
;; the rest as before
(defn ^:export init []
(if (and js/document
(.-getElementById js/document))
;; get loginForm by element id and set its onsubmit property to
;; validate-form function
(let [login-form (by-id "loginForm")]
(set! (.-onsubmit login-form) validate-form))))
;; (set! (.-onload js/window) init)
And here is the related fragment of shopping.cljs
:
;; the rest as before
(defn ^:export init []
(if (and js/document
(.-getElementById js/document))
(let [the-form (by-id "shoppingForm")]
(set! (.-onsubmit the-form) calculate))))
;; (set! (.-onload js/window) init)
Here is the related fragment of index.html
:
<script src="js/main.js"></script>
<script>modern_cljs.login.init();</script>
And here is the related fragment of shopping.html
:
<script src="js/main.js"></script>
<script>modern_cljs.shopping.init();</script>
As you see, by inserting a script snippet in the HTML pages, we're violating the unobtrusive principle expressed by a lot of webapps designers. Life is full of compromises and this is one of those tradeoffs.
All those changes could have been done while the IFDE is running. But there is one more thing we want to take care of and we can't do it while the IFDE is running.
As noted, to adhere to the convention of keeping any JS resourses
confined in a js
subdirectory of the directory serving HTML pages, in
Tutorial-03 we had to create the html/js/main.cljs.edn
file.
Moreover, anytime we create/delete a CLJS namespace, we have to
maintain the require
section of that file. This is a clear case of
incidental complexity introduced by the boot.cljs
task.
Hopefully some day the boot-cljs
maintainers will solve this issue
in a less convoluted way. In the meantime, to bypass that incidental
complexity, we are going to violate the above convention. This is a second
tradeoff. Keep these two tradeoffs in mind, because you've got two
debits that sooner or later you're going to have to pay for.
Let's apply this second tradeoff.
First, delete the html/js/main.cljs.edn
file:
cd /path/to/modern-cljs
rm -rf html/js
Now edit both the html/index.html
and the html/shopping.html
files
to reset the src
attribute of their <script>
tag from js/main.js
to main.js
:
<!doctype html>
<html lang="en">
...
<body>
...
<script src="main.js"></script>
<script>modern_cljs.login.init();</script>
</body>
</html>
<!doctype html>
<html lang="en">
...
<body>
...
<script src="main.js"></script>
<script>modern_cljs.shopping.init();</script>
</body>
</html>
One last thing. In the first tutorial of this series we created
the core.cljs
source file in the src/cljs/modern_cljs
directory. It only prints Hello, world!
in the console of the
browser and it was created just as a kind of a placeholder to show
our IFDE was working. We do not need it anymore and you can safely delete
it:
rm src/cljs/modern_cljs/core.cljs
You can now start the IFDE as usual:
boot dev
...
Compiling ClojureScript...
• main.js
Elapsed time: 35.941 sec
Then visit the http://localhost:3000 and the http://localhost:3000/shopping.html URLs for verifying the two forms are now working as expected.
As usual, if you want to play with the bREPL, launch it as usual and then reload one of the above URLs:
# from a new terminal
cd /path/to/modern-cljs
boot repl -c
...
boot.user=> (start-repl)
...
cljs.user=>
You can now stop any boot
related process and reset your git repository:
git reset --hard
Next step - Tutorial 7: Introducing Domina Events
In the next tutorial we'll introduce domina event handling to further improve our functional style in porting Modern JavaScript samples to CLJS.
Copyright © Mimmo Cosenza, 2012-2015. Released under the Eclipse Public License, the same as Clojure.