Skip to content

Latest commit

 

History

History
254 lines (198 loc) · 8.76 KB

tutorial-06.md

File metadata and controls

254 lines (198 loc) · 8.76 KB

Tutorial 6 - The Easy Made Complex and the Simple Made Easy

In this tutorial we are going to investigate the issue we met in the previous tutorial and try to solve it.

Preamble

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 tutorial-05
git checkout -b tutorial-06-step-1

Introduction

Our latest tutorial ended with a not so nice issue. Just to recap we did as follows:

  • created the login.html page and the corresponding login.cljs source file;
  • created the shopping.html page and the corresponding shopping.cljs source file;
  • launched the app in the usual way
lein ring server # from the project home directory
lein cljsbuild auto # from the project home directory in a new terminal
lein trampoline cljsbuild repl-listen # from the project home directory in a new terminal

We then visited the shopping page in the browser and discovered that the init function we set for the onload property of the JS window object was not the one we defined in shopping.cljs, but the one we defined in login.cljs.

As we anticipated in the previous tutorial, this behaviour depends on the Google Closure Compiler driven by the lein-cljsbuild plugin.

Introducing Google Closure Compiler (GCSL)

In the first tutorial, we set :cljsbuild in project.clj to configure the Google Closure Compiler with the following options:

(defproject ....
  ...
  :cljsbuild {:builds
              [{:source-paths ["src/cljs"]
                :compiler {:output-to "resources/public/js/modern.js"
                           :optimizations :whitespace
                           :pretty-print true}}]})

The :source-paths option instructs GCSL to look for any CLJS source code in the src/cljs directory structure. The :output-to option of the :compiler keyword instructs GCSL to save the compilation result in resources/public/js/modern.js.

I'm not going to explain every single detail of the CLJS/GCSL pair of 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., resources/public/js/modern.js) from all of the CLJS files it finds in the src/cljs directory and subdirectories (i.e., modern.cljs, connect.cljs, login.cljs, and shopping.cljs).

Is mutability evil?

Both login.cljs and shopping.cljs had 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 mutate its previous value: a clear case against JS mutable data structures?

Easy made complex

From the above discussion the reader could infer that that CLJS is good only for a single page browser application. 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.

Here are the bash commands you should enter in the terminal.

mkdir -p src/cljs/{login/modern_cljs,shopping/modern_cljs}
mv src/cljs/modern_cljs/login.cljs src/cljs/login/modern_cljs/
mv src/cljs/modern_cljs/shopping.cljs src/cljs/shopping/modern_cljs/
cp src/cljs/modern_cljs/connect.cljs src/cljs/login/modern_cljs/
cp src/cljs/modern_cljs/connect.cljs src/cljs/shopping/modern_cljs/
rm -rf src/cljs/modern_cljs

And here is the modified fragment of project.clj

(defproject ...
  ...

  :cljsbuild
  {:builds

   ;; login.js build
   {:login
    {:source-paths ["src/cljs/login"]
     :compiler
     {:output-to "resources/public/js/login.js"
      :optimizations :whitespace
      :pretty-print true}}
    ;; shopping.js build
    :shopping
    {:source-paths ["src/cljs/shopping"]
     :compiler
     {:output-to "resources/public/js/shopping.js"
      :optimizations :whitespace
      :pretty-print true}}}})

NOTE 1: To understand the details of the :cljsbuild configurations, I strongly recommend you read the advanced project.clj example from the lein-cljsbuild plugin.

Finally you have to include the right JS file (i.e., js/login.js and js/shopping.js) in the script tag of each html page (i.e., login.html and shopping.html).

Most would call the above solution a kind of incidental complexity. What's worse is the fact that each emitted JS file, no matter how smart the GCSL compiler is in reducing the total size, is different from the others--there is no way for the browser to cache the first file downloaded and serve the others from cache.

Simple made easy

Now the simple made easy way:

  • remove the call (set! (.-onload js/window) init) from both login.cljs and shopping.cljs files;
  • add the :export tag (metadata) to the init function in both login.cljs and shopping.cljs files;
  • add a script tag calling the correponding init function in both login.html and shopping.html files;
  • you're done.

NOTE 2: If you do not ^:export a CLJS function, it will be subject to Google Closure Compiler :optimizations strategies. When set to :simple optimizations, 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() and modern_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 (.getElementById js/document "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 (.getElementById js/document "shoppingForm")]
      (set! (.-onsubmit the-form) calculate))))

;; (set! (.-onload js/window) init)

Here is the related fragment of login.html

    <script src="js/modern.js"></script>
    <script>modern_cljs.login.init();</script>

And here is the related fragment of shopping.html

  <script src="js/modern.js"></script>
  <script>modern_cljs.shopping.init();</script>

You can now run everything as usual:

lein ring server # from the project home directory
lein cljsbuild auto # from the project home directory in a new terminal
lein trampoline cljsbuild repl-listen # from the project home directory in a new terminal

and visit the http://localhost:3000/shopping.html to activate the brepl connection to verify that now you can serve more pages with the same modern.js generated JS file.

ATTENTION NOTE: The brepl connection created via lein trampoline cljsbuild repl-lissen is a little bit unstable. You could need to reload more times the login or the shopping URL to activate/reactivate the brepl connection.

If you created a new git branch as suggested in the preamble of this tutorial, I suggest you commit the changes as follows

git commit -am "Simple made easy."

In a subsequent tutorial we'll introduce domina event management to further improve our functional style in porting Modern JavaScript samples to CLJS.

In the next tutorial we're going to explore CLJS/GCSL compilation modes by using the usual lein-cljsbuild plugin of leiningen.

License

Copyright © Mimmo Cosenza, 2012-2014. Released under the Eclipse Public License, the same as Clojure.