Home / Blog

As I'm progressing with Jwt app development in Clojure, I encountered the question of structuring my code. I'm not yet talking of namespaces, but simply how I should my code in functions, and how do I translate the OO code, heavily using instance variables. See this code from a WT Ruby example to get an idea.

I wanted to develop a login widget with login and password fields and a submit button. I wanted this login widget to be displayed in a modal dialog that disappears when valid credentials are submitted.

I've started with the same approach as my previous post : a function creating the WApplication instance that is passed to the servlet. In this function, I instanciate all main components of the application: the WApplication, the modal dialog, the login widget and the main application screen:

(defn make-login-app [env]
  (let [wapp (new WApplication env)
        root (.getRoot wapp)
        result-text (WText. "")
        user nil
        dialog (WDialog. "test")
        dialog-container (.getContents dialog)
        app-screen (make-app-screen)
        form (make-login-form)
       ]
    ; function code will come here
))

Creating the login form is done by the function make-login-form. This function creates the form elements and lays them out. As Jwt is a framework using the signal/slot idea to implement communication between application components, I've decided to use it with the login form. A signal is triggered when the login is successful, and another signal is triggered when the creadential provided are incorrect.

Using signal is really simple: just instanciate the Signal class and give a way to access this instance to the component wishing to connect to it. In the OO world, this is implemented by an instance variable and a get method. So, how do we translate that in Clojure?

The trick is that the make-login-form will not only return the login widget, but also both signal instances, all placed in a map. That way, inmake-login-app, we will be able to add listeners to these signals.

The signals are triggered by a listener attached to the button's clicked signal: if the credential provided are correct the loggedin-signal is trigger, else the wrong-credentials-signal is triggered.

Here's the function's code (the last line returns the map):

(defn make-login-form []
  (let [layout (WGridLayout.)
        container (WContainerWidget.)
        password-field (doto (WLineEdit. container) 
                  (.setEchoMode WLineEdit$EchoMode/Password ) )
        password #(.getText password-field)
        login-field (WLineEdit. container)
        login #(.getText login-field)
        loggedin-signal (Signal.)
        wrong-credentials-signal (Signal.)
        do-login (fn [evt] 
             (if (authenticate (login) (password)) 
                (.trigger loggedin-signal ) 
                (.trigger wrong-credentials-signal)  ))
        submit-button (WPushButton. "Login")]
    (-> submit-button .clicked
              (.addListener  container 
                   ( create-listener [mouse-event] 
                    (do-login mouse-event) )))
    (.addWidget layout (WLabel. "Login:") 0 0 )
    (.addWidget layout login-field 0 1 )
    (.addWidget layout (WLabel. "Password:") 1 0 )
    (.addWidget layout password-field 1 1)
    (.addWidget layout  submit-button 2 0 1 2)
    (.setLayout container layout)
    (.setFocus login-field)
   { :container container 
    :authenticated loggedin-signal 
    :rejected wrong-credentials-signal}))

The credentials are checked by the function authenticate which takes the login and password as argument. This naive authentication method only to be used in an example will simply check if it finds a user entry with the corresponding user and password (using clj-record):

(defn authenticate [login password]
    (user/find-record {:login login :password password}))

And here we see why I'm so interested in Jwt: I don't have to care about server side or client side! I just code my app and Jwt handles all the rest. I don't have to care about making a request to the server which then will check the credentials in the database, and send a response that I would need to handle at the client side. It's all Clojure code, and Jwt handles all that trasparently.

In the make-login-app I also already initialised the app's main screen, thanks to a function that simply creates a text widget that will be updated by the login widget's signal's listeners:

(defn make-app-screen []
 (let [ container (WContainerWidget.)
        layout (WGridLayout.)
        text (WText. "Hello")]
  (.addWidget layout text 0 0)
  (.setLayout container layout)
  container))

Now that we know how all "variables" are initialised in the make-login-app function, we can take a closer look to that function's body, where we set up the listeners:

(defn make-login-app [env]
  (let [wapp (new WApplication env)
        root (.getRoot wapp)
        result-text (WText. "")
        user nil
        dialog (WDialog. "test")
        dialog-container (.getContents dialog)
        app-screen (make-app-screen)
        form (make-login-form)
       ]
    (.addListener (form :authenticated) wapp  
             (create-listener [] 
               (.remove dialog) 
                (.setText (-> app-screen .getLayout (.getItemAt 0) .getWidget) "Logged in!")))
    (.addListener (form :rejected) wapp  
              (create-listener []  
                 (.setText (-> app-screen .getLayout (.getItemAt 0) .getWidget) "Wrong credentials!")))
    (.setTitle wapp "Login Example")
    (doto dialog (.. getContents (addWidget (form :container))) .show)
    (.addWidget  root app-screen)
    wapp))

We simply add a listener to each signal, which will update the text element we placed in the app-screen. Also note the doto usage: many Jwt methods return void, and the doto macro is really a time-saviour.

Advices and remarks to improve the code are welcome!

Update

Jonathan Smith noted that the multiple calls to (.addWidget layout ...) in make-login-form could be grouped in a doto:

(doto layout
   (.addWidget  (WLabel. "Login:") 0 0 )
    (.addWidget  login-field 0 1 )
    (.addWidget  (WLabel. "Password:") 1 0 )
    (.addWidget  password-field 1 1)
    (.addWidget  submit-button 2 0 1 2))

Jonathan also mentioned the possibility of using a 'continuation passing' style, which rather than using signals, passes functions to be called when an action occurs. The first version of my code was actually like that, and I paste it here for completeness (see the anonymous functions passed to make-login-form):

(defn make-login-form [ succes-fn failure-fn]
  (let [layout (WGridLayout.)
        container (WContainerWidget.)
        password-field (doto (WLineEdit. container) 
           (.setEchoMode WLineEdit$EchoMode/Password ) )
        password #(.getText password-field)
        login-field (WLineEdit. container)
        login #(.getText login-field)
        do-login (fn [evt] (if (authenticate (login) (password)) 
            (succes-fn) (failure-fn)  )) 
        submit-button (WPushButton. "Login")]
    (-> submit-button .clicked 
            (.addListener  container 
              (create-listener [mouse-event] (do-login mouse-event))))
    (.addWidget layout (WLabel. "Login:") 0 0 )
    (.addWidget layout login-field 0 1 )
    (.addWidget layout (WLabel. "Password:") 1 0 )
    (.addWidget layout password-field 1 1)
    (.addWidget layout  submit-button 2 0 1 2)
    (.setLayout container layout)
    (.setFocus login-field)
   container))
    
(defn make-login-app [env]
  (let [wapp (new WApplication env)
        root (.getRoot wapp)
        result-text (WText. "")
        form (make-login-form 
          #(.setText result-text "Welcome back!") 
          #(.setText result-text "Check your credentials!"))
        user nil
        dialog (WDialog. "test")
        dialog-container (.getContents dialog)
       ]
    (.setTitle wapp "Login Example")
    (doto dialog (.. getContents (addWidget form)) .show)
    (.addWidget  root result-text)
    wapp))
</pre>

Disponible pour intervention 

Si vous recherchez quelqu'un pour renforcer temporairement votre équipe ou si vous avez besoin d'aide pour que l'IT aide votre société, n'hésitez pas, contactez-moi !

Lun Mar Merc Jeu Ven Sam Dim
    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31