Skip to content
This repository has been archived by the owner on Mar 7, 2018. It is now read-only.

Intro 3: Replicating the qx.Mobile starter app with ClojureScript

Kenneth Tilton edited this page Jun 21, 2016 · 33 revisions

Welcome to part III of this introduction to programming a mobile device with ClojureScript and Cells. You need not have read Part I, but Part II is quite the prerequisite.

###Beyond "Hi, Mom!" Now that we have "Hi, Mom!" running on your mobile device (or just in your browser if you are content with that), let us do our best to faithfully recreate the starter app provided by qooxdoo when we create (with create-application) a new mobile project.

Again assuming you are continuing on from the prerequisite Part II, let us take a look around in the qooxdoo version. You may have elected to trash the starting point in rube/resources/qxlogin/source/class/qxlogin/Application.js, so here it is again:

      var login = new qxlogin.page.Login();
      var overview = new qxlogin.page.Overview();

      // Add the pages to the page manager.
      var manager = new qx.ui.mobile.page.Manager(false);
      manager.addDetail([
        login,
        overview
      ]);

      // Initialize the application routing
      this.getRouting().onGet("/", this._show, login);
      this.getRouting().onGet("/overview", this._show, overview);

Not too bad, with much encapsulated in the Login and Overview class definitions. Two can play at that game, but with Qxia and Cells in general we need not be forever defining new classes. Instead, we compose with functions returning stock* qooxdoo entities which get intelligently assembled by Qxia internals (which are quite simple if you choose to dig into the observe methods on the all-import :kids slot).

  • Note that one win from using Cells is that widgets are now an order of magnitude more re-usable, because different properties of different instances can be expressed with different rules and now, with Cells 4, different observers and change detectors. Oh, and in the land of Clojure* and Javascript we can define new properties for instances as we like.

Please edit loginclj.cljs to be this in its entirety:

 (ns loginclj.core
  (:require
   [tiltontec.model.core :refer-macros [c?kids]]
   [tiltontec.qxia.types :as qxty]
   [tiltontec.qxia.core :refer [label qx-make button routing-get] ]
   [tiltontec.qxia.macros :refer-macros [navigation-page]]))

(declare make-login make-overview)

(defn ^:export loginclj [this shower]
  (qx-make ::qxty/Mobile
    :qx-me this
    :pager (new js/qx.ui.mobile.page.Manager false)
    :shower shower
    :kids (c?kids
            (make-login)
            (make-overview))))

(defn make-login []
  (navigation-page ["Login" "/"][]
    (button "Login"
      :listeners {"tap" #(routing-get "/overview")})))

(defn make-overview []
  (navigation-page ["Overview" "/overview"]
    [:showBackButton true
     :backButtonText "Back"]
    (label "<h1>Our first app!</h>")))

Some notes:

  • :qx-me this -- ClojureScript/Qxia models need to know the qooxdoo instance they are controlling;
  • (c?kids ...) or (c? (the-kids ...)) -- both allow container children to be computed by a rule which also crucially defers their creation until the right time to make both Cells and qooxdoo happy;
  • Passing false to the page manager says "this is not a tablet". The qooxdoo doc suggests passing null would cause qooxdoo to decide the device itself. I have not had great luck doig that. Left as exercise;
  • The two parameters to navigation-page become the title and end-point;
  • routing-get hides just a few lines of boilerplate which take care of page navigation in the qooxdoo framework;
  • :showBackButton and :backButtonText are navigation page properties passed straight through to qooxdoo;
  • I do not mean to tell you my problems (as a Qxia developer) but if you check out tiltontec.qxia.widget.cljs you will find qx-initialize and observe :kids specialized on m.NavigationPage, ironing out some idiosyncracies in their assembly.

If you have this running in a shell:

lein cljsbuild auto qxlogin

...you can save the above, reload the source index.html in your browser and see that the first page now has a "Login" button to take us to the overview page.

There we encounter a problem: we see the back button but it does not respond to clicks. You can hit backspace and navigate back to the first page, but what is up with the clicking? #####The qooxdoo page back button does not respond to clicks If you have played with the qooxdoo starter app in the web test mode, you might have noticed that the back button (unsurprisingly) works if clicked. Surprisingly, this is not true if one builds one's own version, because the involved class qx.ui.mobile.page.NavigationPage lacks a little hack I found in class/pages/Overview.js, the demo page generated by default.

But hey, what is open source for? I copied their hack into a clone called NaviBack.js and got on with my life.

Here is how you can do the same (deliberately triggering an error when we first load the new page just to get used to the error):

cd rube/resources/qxlogin/source/class/qxlogin
cp rube/resources/qxialib/NaviBack.js .

Now edit qxlogin/.../NaviBack.js and change the class name to match its relative location:

qx.Class.define("qxlogin.NaviBack",
{ ...etc...

...and finally make use of that class on the overview page, modifying that source to look like this:

(defn make-overview []
  (navigation-page ["Overview" "/overview"]
    [:showBackButton true
     :backButtonText "Back"
     :qx-class js/qxlogin.NaviBack] ;; <-- override
    (label "<h1>Our first app!</h>")))

And now we should see an error if we reload the page with the browser debug console open, because qooxdoo did not see our CLJS code specifying qxlogin.NaviBack:

ERROR! qx-class-new> key class specified but nil Do we need a new qx class mention in Application.

I need to tighten up that message, grammar and punctuation, but you might also wonder why the error does not mention the class name NaviBack. From the above example, compilation to CLJS produces :qx-class nil so that information is lost to us. (We could preserve it with sufficient legerdemain, but the message itself will tell the experienced Qxia developer what they forgot to do, or what they misspelled.)

Anyway, this is one possible error we can get if we forget to maintain config.json when bringing new classes into play. Please edit rube/resources/qxlogin/config.json and make another entry in the =include property of the mobile-common entry:

    "mobile-common" :
    {
      "=include" :
      [
        "${APPLICATION}.Application"
        , "qx.ui.mobile.*"
        , "qxlogin.NaviBack" // <---- insert 
      ],

      "environment" :...etc 

and now re-generate

./generate.py source

####A proper (but still forgiving) login interface. Now let us get more serious about the login.

You can view the definitions of the login and overview pages in, well, the pages subdirectory. Here is the beef from the login class:

      // Username
      var user = new qx.ui.mobile.form.TextField();
      user.setRequired(true);

      // Password
      var pwd = new qx.ui.mobile.form.PasswordField();
      pwd.setRequired(true);

      // Login Button
      var loginButton = new qx.ui.mobile.form.Button("Login");
      loginButton.addListener("tap", this._onButtonTap, this);

      var loginForm = this.__form = new qx.ui.mobile.form.Form();
      loginForm.add(user, "Username");
      loginForm.add(pwd, "Password");

      // Use form renderer
      this.getContent().add(new qx.ui.mobile.form.renderer.Single(loginForm));
      this.getContent().add(loginButton);

We already have the login button. Above that, please add:

   (form [][:name :login]
      (text-field "Username"
         :placeholder "Just type something"
         :required true
         :requiredInvalidMessage "Please share your user name")

     (qx-make ::qxty/m.PasswordField
      :label "Password"
      :placeholder "Just type something"
      :required true
      :requiredInvalidMessage "Password is required"))

and now we need to refer some more things in our namespace. Here is the whole shebang for your convenience (with a few extras we will be needing shortly):

 (ns loginclj.core
  (:require
   [tiltontec.cell.core
    :refer-macros [c?]
    :refer [c-in]]
   [tiltontec.model.core
    :refer-macros [c?kids]
    :refer [fm! md-reset! md-get fget]]
   [tiltontec.qxia.types :as qxty]
   [tiltontec.qxia.base  :refer [qxme]]
   [tiltontec.qxia.core
    :refer [routing-get qx-make button label text-field]]
   [tiltontec.qxia.macros
    :refer-macros [group hbox vbox navigation-page form]]))

Save that, confirm the auto-compile went OK, and reload the app in your browser.

You should see the two new fields, but they are not doing much: leave them blank and you can still "Log In", even though we declared each to be required. Hunh?

Welcome to the qx.Mobile form. They are actually non-UI widgets, making things tough on us Qxia internals developers. They also put validation under our control, not enforcing :required true until asked.

Let us ask, in a new button definition:

    (button "Login"
      :listeners {"tap" (fn [evt me]
                          (let [login (qxme (fget :login me))]
                            (when (.validate login) ;; <-- we ask
                              (routing-get "/overview"))))})

Now you should get prominent errors if you leave either field blank, and now we have recreated the qooxdoo starter app.

Two quick notes:

  • The anonymous function expects two parameters, the event and me. In Qxia, by convention me will be the atom wrapping an actual qooxdoo instance found in the :qx-me property of the atom's map. How did that get passed to our handler? Qxia internals wrap all declared :listeners handlers in a true handler that injects me in the call to our handler.
  • fget is short for "family get". The first parameter specifies for what name or type we are looking, the second parameter tells fget where to begin its search. fget is one function the Qxia/Cells developer must go to school on (especially to avoid infinite recursion blowing out the stack), but we will defer that seminar.

####Extra Credit Now let us have some dataflow fun, realistic or not. We start by making the password field a bit fancier, including giving it a name:

(qx-make ::qxty/m.PasswordField
         :name :p-word
         :label "Password"
         :value (c-in nil)
         :placeholder "Just type something"
         :required true
         :requiredInvalidMessage "Password is required"
         :liveUpdate true
         :listeners {"changeValue"
                     (fn [evt me]
                       (let [new (.getData evt)]
                         (md-reset! me :value new)))})

About that code:

  • Until now we have not been moving the value from the qooxdoo widget to our Qxia CLJS wrapper. The changeValue listener sees to that using the API to kick off propagation of the change;
  • :liveUpdate true requests a changeValue event for each keystroke; and
  • we name the field :p-word.

And now the fun bit. Let's disable the login button if the password is blank (and give it a name we will use shortly):

    (button "Login"
      :name :login-button
      :enabled (c? (pos? (count (md-get (fget :p-word me) :value))))
      :listeners...etc

Now you can see the Login button enabled/disabled as you populate/clear the password. Things to note:

  • in a moment we will reduce that long expression retrieving the password's value to (f-val :p-word :value me). We could have done that sooner but I have trouble with tutorials that hide moving parts too soon;
  • there is no explicit dependency management. Dependencies are identified automatically (as long as we do not bypass md-get with the backdoor (:value @some-model-atom));
  • it may not jump out at us, but there is no need to worry even in our non-determinism about whether the :p-word instance exists when our :enabled rule fires: the :kids slot itself is rule-based. As the fget search asks for the :kids of a model they will get computed and initialized JIT*
    • I did not think it would work, either.

We can also see widgets come and go based on other model state. First, please add this just before make-login:

(defn f-val [what slot where]
  (let [it (fget what where)]
    (assert it (str "cannot find " what " starting " where "in search of " slot))
    (md-get it slot)))

That quick hack* will let us reduce the boilerplate as we add this demonstration widgetry right after the button:

    (group []
      (when-not (f-val :login-button :enabled me)
        (label "We will enable the login button once you start the password"))
      (label (c? (str "I am thinking of a number: " (rand-int 1000)))))
  • A better hack will soon be added to Qxia as a macro so we can optionally capture me from the lexical context.

Note that the macro group wraps its body in a c? formula, so the dependency on the enabled slot triggers only the recalculation and regeneration of the group contents. You can observe that when you start typing: the hint disappears and the random number changes because a new label is created as well, but then as you continue typing the random number does not change because the enabled flag, although being recomputed on each keystroke, does not change (until the last character is deleted). And the other widgets live on unaffected.

To see a less efficient alternative, add this almost identical group after the first:

    (group []
      (when-not (pos? (count (f-val :p-word :value me)))
        (label "We will enable the login button once you start the password"))
      (label (c? (str "I am thinking of a number: " (rand-int 1000)))))

One nice thing here is that coding sensibly produces efficient dependencies: the hint about the button being enabled should be driven by the state of the enabled button, not the amount of text somewhere; what if the enabled rule got more interesting and pulled in other dependencies?

Similarly, a rule such as (if A B C) where A, B, and C are Cells will never depend on B and C simultaneously, but if we lose our functional style and code (let [b B c C] (if A a b) we will depend needlessly on B or C. I have always gotten a kick out of Cells rewarding style.

####Back to our regularly scheduled mobile app Let us see if it still runs on our Android device:

cd rube/resources/qxlogin
./generate.py build
cd rube/mylogin/www
mv index.html i.bak
rsync -r ../../resources/qxlogin/build/* .
mv i.bak index.html
cordova build
cordova run android

Works for me, but if you have problems please let me know and I will be happy to resolve them.

Next up: iOS (and another crack at PhoneGap). This will mean (a) finding my MacBook and (b) starting from scratch since I have never used that for development. See you tomorrow.

[Update: Or this afternoon. I went with the PhoneGap IDE on Mac OS X, copied over the assets from the Cordova WWW directory on a USB stick, and It Just Worked(tm).]