⬆️ ⬇️

Clojure web application. Part 2

Hello, dear users and visitors of Habr. In the first article, Clojure Web Applications reviewed the basic tools and libraries for building Clojure web projects. Namely Leiningen, Ring, Compojure, Monger, Buddy and Selmer. Here we will discuss their practical application.



For those who are set to self-development, through the comprehension of the code to bypass the reading of the article - I ask at the bottom of the page for a project reference to Github.



Introduction


And so let's start in order. To make it more interesting for you, I decided to choose a more or less applied focus of the article. Today we will create a simple Clojure web application that will manage notes. I assume that you already have Leiningen, Clojure and MongoDB installed (do not forget to enable it). The lion's share of the content is directly in the comments in the code, which for your convenience is hidden in the spoilers.






IDE


For Clojure there are a lot of different editors and IDE, in this article I will not give their pros and cons, more generally I will not once. Everyone has different preferences, what to use is up to you. I use LightTable which is written in ClojureScript and is completely satisfied with it, there are a large number of modules for it, out of the box it has everything you need to start developing on Clojure and ClojureScript, it contains the ParEdit module. You do not have to configure anything to connect to the project remotely or locally via repl. Interaction with repl in LightTable is very peculiar, in my subjective opinion it is very convenient - you can call functions and view their results in a separate window in live mode (such as in Emacs and in all other IDEs) or do the same directly in the code, it’s enough to translate the cursor on the first or last bracket of the expression and press cmd + enter (MacOS), then LightTable will create the repl connection and compile this expression, you need to enter the name of the compiled function or a variable below the line and view its result directly in the code .




')

Back-end



Project


First of all, create our project: $ lein new compojure notes



Now we have a directory with the preparation of our application. Let's go into it and open the project.clj file in the editor. You need to add to it depending on the libraries we use:



project.clj
(defproject notes "0.1.0-SNAPSHOT" :description " " :min-lein-version "2.0.0" :dependencies [; -,  Clojure   ;   [org.clojure/clojure "1.6.0"] ;   GET  POST  [compojure "1.3.1"] ;  (middleware)   ;  [ring/ring-defaults "0.1.5"] ;  [selmer "0.8.2"] ;  Monger [com.novemberain/monger "2.0.1"] ;    [clojure.joda-time "0.6.0"]] ;  -     ;  ,     ; Ring'     - Jetty :plugins [[lein-ring "0.8.13"]] ;    Ring  ;   app  ;        :ring {:handler notes.handler/app} :profiles {:dev {:dependencies [[javax.servlet/servlet-api "2.5"] [ring-mock "0.1.5"]]}}) 






When you start a project, Leiningen will install all the specified dependencies automatically, all you need to do is specify them in project.clj and be connected to the Internet. Next, let's move on to creating the server part of our application. It is more logical to place the functions of mapping, working with the database, handlers and routes on separate files so that there are no conflicts of names and brain-fucking'a inconvenience, but it is as you like.






Handler (main processor)


Let's start with handler.clj, Lein has already taken care of its creation and it is located in the / src / notes directory (it also contains all of our Clojure code). This is an important part of our application. It contains the app variable, which includes the routes of our application and basic middleware (a wrapper for requests and responses) for HTTP. Add the routes of our application to the namespace, we get the following code:



handler.clj
 (ns notes.handler (:require ;   [notes.routes :refer [notes-routes]] ;   middleware [ring.middleware.defaults :refer [wrap-defaults site-defaults]])) ;    middleware (def app (wrap-defaults notes-routes site-defaults)) 









Routes


Now we will create the routes.clj file, we will place our routes in it - which, when requested using the GET and POST methods using the specified URI, will call the function of form handlers, and page mappings. In this part of the application, we use the Compojure API. I immediately apologize for the huge chunks of code for those who are embarrassed by them, but I added a lot of comments in them to make it easier for you to understand the logic of their work:



routes.clj
 (ns notes.routes (:require ;    [compojure.core :refer [defroutes GET POST]] [compojure.route :as route] ;   [notes.controllers :as c] ;   [notes.views :as v] ;      [notes.db :as db])) ;   (defroutes notes-routes ;    (GET "/note/:id" [id] ;      ObjectId ;      (let [note (db/get-note id)] (v/note note))) ;      ObjectId (GET "/delete/:id" [id] (c/delete id)) ;    (POST "/edit/:id" request (-> c/edit)) ;    ;  ,   ; ObjectId    ;  ,    ;  . (GET "/edit/:id" [id] ;      ObjectId ;      (let [note (db/get-note id)] (v/edit note))) ;    (POST "/create" ;      ;   [title text],    ; request    ;      request ;     ; : (create-controller request) (-> c/create)) ;    (GET "/create" [] (v/create)) ;    (GET "/" [] ;     ;    fn  (let [notes (db/get-notes)] (v/index notes))) ;  404 (route/not-found "  ")) 









Controllers (form processing)


To handle POST, sometimes GET requests, in the routes above we use the so-called functions “controllers” (handlers), put them into a separate file. Here I deliberately omit the full validation of the validity of the input data as it deserves a separate article. Name this file controllers.clj, its contents are as follows:



controllers.clj
 (ns notes.controllers (:require ;   [ring.util.response :refer [redirect]] ;      [notes.db :as db])) (defn delete "  " [id] (do (db/remove-note id) (redirect "/"))) (defn edit "  " [request] ;     (let [note-id (get-in request [:form-params "id"]) note {:title (get-in request [:form-params "title"]) :text (get-in request [:form-params "text"])}] ;   (if (and (not-empty (:title note)) (not-empty (:text note))) ;    ;     ;   ;    (do (db/update-note note-id note) (redirect "/")) ;      "   "))) (defn create "  " [request] ;     ;     ;    hash-map ; ( ) (let [note {:title (get-in request [:form-params "title"]) :text (get-in request [:form-params "text"])}] ;   (if (and (not-empty (:title note)) (not-empty (:text note))) ;    ;     ;   ;    (do (db/create-note note) (redirect "/")) ;      "   "))) 









DB (MongoDB interaction)


A very interesting part of the application, the Monger library will help us in its construction. Create a file db.clj, it will contain functions for interacting with MongoDB. Of course, we can call Monger functions directly in routes and controllers, but for this we will receive retribution in debugging, expansion along with a bunch of duplicate code, thereby multiplying the final number of lines of code. Monger also allows you to make queries to MongoDB via DSL queries (for relational databases, there is an excellent sqlcorma library), this is very convenient for complex queries, but in this article I will not describe them. Let's add functions to db.clj:



db.clj
 (ns notes.db (:require ;  Monger monger.joda-time ;      [monger.core :as mg] [monger.collection :as m] [monger.operators :refer :all] ;    [joda-time :as t]) ;    Java  (:import org.bson.types.ObjectId org.joda.time.DateTimeZone)) ;        (DateTimeZone/setDefault DateTimeZone/UTC) ;      (defonce db (let [uri "mongodb://127.0.0.1/notes_db" {:keys [db]} (mg/connect-via-uri uri)] db)) ;        (defn- date-time "   " [] (t/date-time)) (defn remove-note "    ObjectId" [id] ;    ObjectId (let [id (ObjectId. id)] (m/remove-by-id db "notes" id))) (defn update-note "    ObjectId" [id note] ;    ObjectId (let [id (ObjectId. id)] ;     $set ;        ;       ;      ;   hash-map +    ;       ;     . ; -    ;   ObjectId   ;  update-by-id, ;      ;    (m/update db "notes" {:id id} ;    ;    {$set (assoc note :created (date-time))}))) (defn get-note "    ObjectId" [id] ;      :_id ;      ;     ObjectId ;   ,  ;     ObjectId (let [id (ObjectId. id)] ;    hash-map   (m/find-map-by-id db "notes" id))) (defn get-notes "  " [] ; Find-maps    ;     hash-map (m/find-maps db "notes")) (defn create-note "   " ;      ;    hash-map c : ; {:title "" :text ""} [note] ; Monger    ObjectId ;     ;     (let [object-id (ObjectId.)] ;     hash-map ;   ,  ;     ObjectId ;       (m/insert db "notes" (assoc note :_id object-id :created (date-time))))) 









Views (HTML template presentation)


In the views.clj file, we will place functions that display HTML templates and pass data to them. The Selmer library, inspired by the data presentation system in Django templates, will help us. Selmer also allows you to add filters (functions) for processing data in the template itself, tags, and provides flexible settings for yourself. Let's write the page display functions:



views.clj
 (ns notes.views (:require ; "" [selmer.parser :as parser] [selmer.filters :as filters] ;    [joda-time :as t] ;  HTTP  [ring.util.response :refer [content-type response]] ;  CSRF  [ring.util.anti-forgery :refer [anti-forgery-field]])) ;  Selmer     (parser/set-resource-path! (clojure.java.io/resource "templates")) ;     -  (defn format-date-and-time "   " [date] (let [formatter (t/formatter "yyyy-MM-dd  H:m:s" :date-time)] (when date (t/print formatter date)))) ;       (filters/add-filter! :format-datetime (fn [content] [:safe (format-date-and-time content)])) ;           ;     anti-forgery  (parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field))) (defn render [template & [params]] "     html      " (-> template (parser/render-file ;      ;      ;    (assoc params :title " " :page (str template))) ;     HTTP  response (content-type "text/html; charset=utf-8"))) (defn note "  " [note] (render "note.html" ;     {:note note})) (defn edit "  " [note] (render "edit.html" ;     {:note note})) (defn create "  " [] (render "create.html")) (defn index "  .  " [notes] (render "index.html" ;     ;  notes   false {:notes (if (not-empty notes) notes false)})) 









Front-end



HTML templates


Our application is almost ready, it remains to create HTML files in which data will be displayed. The / resources directory is required to host static files, i.e. even after compiling the application in a .jar or .war file, we can replace the files in it. In the public in the next article we will add CSS tables. In the meantime, create a templates directory where we place the HTML files.








First of all, we will create a base file for all templates, it will contain the main markup for all pages and the content block in which the markup of the remaining sections will be placed. Let's start:



base.html
 <!DOCTYPE html> <html> <head> <META http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>{{title}}</title> </head> <body> <ul> <li> {% ifequal page "index.html" %} <strong> </strong> {% else %} <a href="/"> </a> {% endifequal %} </li> <li> {% ifequal page "create.html" %} <strong> </strong> {% else %} <a href="/create"> </a> {% endifequal %} </li> </ul> {% block content %} {% endblock %} </body> </html> 









Now we will impose a template with a form for creating a note. In it, as in all forms of our application, you need to add the tag {% csrf-field%}, which we created in view.clj, otherwise when you send the form we get an error Invalid anti-forgery token. Let's start:



create.html
 {% extends "base.html" %} {% block content %} <h1> </h1> <form action="POST"> {% csrf-field %} <p> <label></label><br> <input type="text" name="title" placeholder=""> </p> <p> <label></label><br> <textarea name="text"></textarea> </p> <input type="submit" value=""> </form> {% endblock %} 









We already have a route, a view and a handler for editing a note, let's create a template with a form for all of this:



edit.html
 {% extends "base.html" %} {% block content %} <h1> </h1> <form method="POST"> {% csrf-field %} <input type="hidden" name="id" value="{{note._id}}"> <p> <label></label><br> <input type="text" name="title" value="{{note.title}}"> </p> <p> <label></label><br> <textarea name="text">{{note.text}}</textarea> </p> <input type="submit" value=""> </form> {% endblock %} 









Next, we will impose a template for viewing the note, in which the attentive reader in the small tag will see a strange data view, this is something else like our filter created in view.clj. To call filters in Selmer, it is customary to write: {{variable | filter}}. Now the code itself:



note.html
 {% extends "base.html" %} {% block content %} <h1>{{note.title}}</h1> <small>{{note.created|format-datetime}}</small> <p>{{note.text}}</p> {% endblock %} 









And finally, our main page, where the list of notes will be displayed:



note.html
 {% extends "base.html" %} {% block content %} <h1></h1> {% if notes %} <ul> {% for note in notes %} <li> <h4><a href="/note/{{note._id}}">{{note.title}}</a></h4> <small>{{note.created|format-datetime}}</small> <hr> <a href="/edit/{{note._id}}"></a> | <a href="/delete/{{note._id}}"></a> </li> {% endfor %} </ul> {% else %} <strong>  </strong> {% endif %} {% endblock %} 









Conclusion


Now our application is ready, of course we missed a very important step - testing the functions in the repl and viewing their results, but in the next article we will focus on it in detail.



Let's run: $ lein ring server



This command, like lein run, installs all dependencies, runs our application on Ring's basic Jetty web server at localhost: 3000. I note that while we can not compile our application in a .jar or .war file or run it through lein run.






Additional links






In the next article we will add the immutant web server to our web application, and implement auto-update code “on the fly” i.e. as files are saved in the editor, our server will automatically update them and we will be able to see the results of the code change after reloading the page and not the server as it is now. At the moment, on the fly, we can only change HTML files. And we’ll also add classes to our HTML Bootstrap, since the layout looks really not fun, despite the fact that the essence of the articles is not in a beautiful design, I think you shouldn’t go back to WEB 1.0.



Despite the close check before publishing the article, I will be grateful if you notice inaccuracies in the description or grammar errors and let me know about it in the messages. I would also like to thank the people who showed interest in my first article, I’m still happy to tell you about Clojure web development. I say goodbye to this and wish you all the best!

Source: https://habr.com/ru/post/263131/



All Articles