How to display content in a ClojureScript application based on response from server?

I want ClojureScript application

  1. send a HTTP GET request to the backend,
  2. receive a piece of Hiccup as text,
  3. parse it, and
  4. display the resulting HTML code.

Note that the purpose of this is to explore some ideas. This code will never be put in production.

How can I do all of the above?

Here is what I tried.

I can use

(clojure.edn/read-string (str "  [:ol "
"   [:li \"A\"]"
"   [:li \"B\"]"
"   [:li \"C\"]"
"   ]")

to transform a Hiccup string into a form which can be displayed using

(defn Application []
  (clojure.edn/read-string (str "  [:ol "
"   [:li \"A\"]"
"   [:li \"B\"]"
"   [:li \"C\"]"
"   ]"))

The next step is to replace the hardcoded Hiccup string

(str "  [:ol "
"   [:li \"A\"]"
"   [:li \"B\"]"
"   [:li \"C\"]"
"   ]")

with the response from server.

I tried to first save the result of the request to http://localhost:8081/lc-app/1/ui/main in a variable body.

(def body (go (let [response (<! 
  (http/get "http://localhost:8081/lc-app/1/ui/main"
                                               {:with-credentials? false}))
                        body (:body response)
          success (:success response)
          hiccup (if success
                       "[:p \"Error\"]")
                (println (str "body: " hiccup))

The intention is that if the request suceeds, body will be equal to the hiccup text from the server, and if it fails to [:p \"Error\"].

The request itself goes through without problems.

Then I need to replace the definition of Application.

(defn Application []
  (clojure.edn/read-string body))

When I do this, I get the following error:

Uncaught Error: No protocol method ICounted.-count defined for type cljs.core.async.impl.channels/ManyToManyChannel: [object Object]
    cljs$core$missing_protocol core.cljs:324
    cljs$core$ICounted$_count$dyn_11470 core.cljs:585
    cljs$core$_count core.cljs:585
    cljs$core$count core.cljs:1850
    cljs$tools$reader$reader_types$string_reader reader_types.cljs:215
    cljs$core$IFn$_invoke$arity$2 reader_types.cljs:222
    cljs$core$IFn$_invoke$arity$1 reader_types.cljs:220
    cljs$core$IFn$_invoke$arity$2 edn.cljs:446
    cljs$core$IFn$_invoke$arity$1 reader.cljs:183
    cljs$core$IFn$_invoke$arity$1 edn.cljs:53
    lcp_front$app$Application app.cljs:24
    res component.cljs:86
    reagent$impl$component$wrap_render reagent.impl.component.js:126
    reagent$impl$component$do_render component.cljs:117
    _render component.cljs:260
    reagent$ratom$in_context ratom.cljs:44
    reagent$ratom$deref_capture ratom.cljs:57
    reagent$ratom$run_in_reaction ratom.cljs:539
    reagent$impl$component$wrap_funs_$_render component.cljs:260
    React 16
    reagent$dom$render_comp dom.cljs:17
    cljs$core$IFn$_invoke$arity$3 dom.cljs:51
    cljs$core$IFn$_invoke$arity$2 dom.cljs:37
    lcp_front$app$init app.cljs:32
    <anonymous> shadow.module.main.append.js:4

How can I fix it, i. e. how can I

  1. get the Hiccup string from the server and
  2. render it inside Application?

You get that JS error because go returns not a value but a channel that might at some later point have that value. You can’t block in a browser, so you have to apply some async approach. E.g. in this case your Hiccup could be in a Reagent’s atom and then, when the request succeeds, you reset! that value with the received one.

I don’t know why you’re getting [:p "Error"] though - perhaps, there’s no :success key in the response? I’d js/console.log the whole response and check what keys and values it has (make sure to use cljs-devtools to get CLJS data structures properly rendered in your browser’s JS console).

1 Like

@p-himik Thanks.

I modified the code as follows. I think it’s along the lines you suggested.

  (:require-macros [cljs.core.async.macros :refer [go]])
  (:require [reagent.core :as r])
  (:require [reagent.dom  :as dom])
  (:require [clojure.edn])
  (:require [cljs-http.client :as http]
            [cljs.core.async :refer [<!  alts!]]))

(def body-response (atom nil))

(defn get-body

    (let [
          response (<! (http/get "http://localhost:8081/lc-app/1/ui/main"))]
      (println response)
      (reset! body-response response))))

(defn render-body
    (let [r @body-response
          body (:body r)]
    (clojure.edn/read-string body)))

(defn Application []

(defn init []
  (println "The app has started!")
  (dom/render [Application] (js/document.getElementById "app")))

Now I am getting a CORS-related error.

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:8081/lc-app/1/ui/main. (Reason: expected 'true' in CORS header 'Access-Control-Allow-Credentials').

The server is configured to accept requests from http://localhost:8080. Therefore I guess that this error occurs on the ClojureScript client.

If you know how to fix it, please tell.

But the error mentions port 8081, not 8080.

8081 is the port of the Pedestal backend.

The ClojureScript frontend on port 8080 sends a GET request to the backend server on port 8081.

The server on the 8081 port is configured as shown below.

(ns lcp-backs.service
  (:require [io.pedestal.http :as http]
            [io.pedestal.http.route :as route]
            [io.pedestal.http.body-params :as body-params]
            [ring.util.response :as ring-resp]))

(defn about-page
  (ring-resp/response (format "Clojure %s - served from %s"
                              (route/url-for ::about-page))))

(defn home-page
  (ring-resp/response "Hello World!"))

(defn app-1-ui-main
             "  [:ol "
             "   [:li \"A\"]"
             "   [:li \"B\"]"
             "   [:li \"C\"]"
             "   ]"))


;; Defines "/" and "/about" routes with their associated :get handlers.
;; The interceptors defined after the verb map (e.g., {:get home-page}
;; apply to / and its children (/about).
(def common-interceptors [(body-params/body-params) http/html-body])

;; Tabular routes
(def routes #{["/" :get (conj common-interceptors `home-page)]
              ["/about" :get (conj common-interceptors `about-page)]
              ["/lc-app/1/ui/main" :get (conj common-interceptors `app-1-ui-main)]


;; Map-based routes
;(def routes `{"/" {:interceptors [(body-params/body-params) http/html-body]
;                   :get home-page
;                   "/about" {:get about-page}}})

;; Terse/Vector-based routes
;(def routes
;  `[[["/" {:get home-page}
;      ^:interceptors [(body-params/body-params) http/html-body]
;      ["/about" {:get about-page}]]]])

;; Consumed by lcp-backs.server/create-server
;; See http/default-interceptors for additional options you can configure
(def service {:env :prod
              ;; You can bring your own non-default interceptors. Make
              ;; sure you include routing and set it up right for
              ;; dev-mode. If you do, many other keys for configuring
              ;; default interceptors will be ignored.
              ;; ::http/interceptors []
              ::http/routes routes

              ;; Uncomment next line to enable CORS support, add
              ;; string(s) specifying scheme, host and port for
              ;; allowed source(s):
              ;; "http://localhost:8080"
              ::http/allowed-origins ["http://localhost:8080"]

              ;; Tune the Secure Headers
              ;; and specifically the Content Security Policy appropriate to your service/application
              ;; For more information, see:
              ;;   See also:
              ;;::http/secure-headers {:content-security-policy-settings {:object-src "'none'"
              ;;                                                          :script-src "'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:"
              ;;                                                          :frame-ancestors "'none'"}}

              ;; Root for resource interceptor that is available by default.
              ::http/resource-path "/public"

              ;; Either :jetty, :immutant or :tomcat (see comments in project.clj)
              ;;  This can also be your own chain provider/server-fn --
              ::http/type :jetty
              ;;::http/host "localhost"
              ::http/port 8081
              ;; Options to pass to the container (Jetty)
              ::http/container-options {:h2c? true
                                        :h2? false
                                        ;:keystore "test/hp/keystore.jks"
                                        ;:key-password "password"
                                        ;:ssl-port 8443
                                        :ssl? false
                                        ;; Alternatively, You can specify your own Jetty HTTPConfiguration
                                        ;; via the `:io.pedestal.http.jetty/http-configuration` container option.
                                        ;:io.pedestal.http.jetty/http-configuration (org.eclipse.jetty.server.HttpConfiguration.)

If you are interested: Adding {:with-credentials? false} to

(http/get "http://localhost:8081/lc-app/1/ui/main" 
  {:with-credentials? false} )

fixed the problem.