How do I edit Reitit routes in Reagent? - clojurescript

The routes created with the default reagent template look like this:
;; -------------------------
;; Routes
(def router
(reitit/router
[["/" :index]
["/items"
["" :items]
["/:item-id" :item]]
["/about" :about]]))
If I change the path of one ("/about" to "/test" below), why does it no longer work? There must be something else driving the routing, but I can't seem to figure out what.
;; -------------------------
;; Routes
(def router
(reitit/router
[["/" :index]
["/items"
["" :items]
["/:item-id" :item]]
["/test" :about]]))
This is the default reagent template (lein new reagent...) and I haven't changed anything else in the code. Any help would be greatly appreciated.
Edit - Some additional detail
I poked around in the repl a little bit in this function (from the default template):
(defn init! []
(clerk/initialize!)
(accountant/configure-navigation!
{:nav-handler
(fn [path]
(let [match (reitit/match-by-path router path)
current-page (:name (:data match))
route-params (:path-params match)]
(reagent/after-render clerk/after-render!)
(session/put! :route {:current-page (page-for current-page)
:route-params route-params})
(clerk/navigate-page! path)
))
:path-exists?
(fn [path]
(boolean (reitit/match-by-path router path)))})
(accountant/dispatch-current!)
(mount-root))
Everything looks ok to me. In fact, executing the below steps in the repl successfully navigated the browser to the correct page. I still can't enter the URL directly though.
app:problem.core=> (require '[reitit.frontend :as reitit])
nil
app:problem.core=> (reitit/match-by-path router "/test")
{:template "/test",
:data {:name :about},
:result nil,
:path-params {},
:path "/test",
:query-params {},
:parameters {:path {}, :query {}}}
app:problem.core=> (def match (reitit/match-by-path router "/test"))
#'problem.core/match
app:problem.core=> (:name (:data match))
:about
app:problem.core=> (:path-params match)
{}
app:problem.core=> (def current-page (:name (:data match)))
#'problem.core/current-page
app:problem.core=> (page-for current-page)
#'problem.core/about-page
app:problem.core=> (session/put! :route {:current-page (page-for current-page) :route-params {}})
{:route {:current-page #'problem.core/about-page, :route-params {}}}
app:problem.core=>

It looks like you changed the routes on client-side, in src/cljs/<project_name>/core.cljs, but did not change them server side in src/clj/<project_name>/handler.clj (look under the def app near the bottom of the file).
If your new to developing web applications with Clojure, I'd recommend looking at Luminus, rather than using the Reagent template. It's a much more batteries included-approach, with a lot more documentation. The book "Web Development With Clojure" is written by the same author (who is also a contributor to Reagent), and is also recommended reading.

Related

ClojureScript - Ajax - Retrieving json

I have my dependencies
(ns test.core
(:require [reagent.core :as reagent :refer [atom]]
[ajax.core :refer [GET]]))
I then have my handler that handles the responses to my ajax call
(defn handle-response [response]
(println (type response))
(println film))
THe data from my call is JSON, in the browser it looks like this:
{"id":3,"name":"Sicario","star":"Emily Blunt","rating":5}
When I run the above Clojure, I see this:
cljs.core/PersistentArrayMap core.cljs:192:23
{id 3, name Sicario, star Emily Blunt, rating 5}
Is their a way for me to go from the PersistArrayMap, and destructure it into id, name, star and rating?
I tried
(let [film (js->clj (.parse js/JSON response) :keywordize-keys true)]
...)
Hoping I would get a map named film, but no avail!
I think maybe I could use (get-in), but can't get the syntax right for this either.
Thanks,
Got it,
I was missing :response-format :json and :keywords? true from my GET call
Now, it looks like this:
(GET (str "https://localhost:5001/api/films/" film-name)
{:response-format :json
:keywords? true
And it all works.

Clojure encode Joda DateTime with ring-json

With the following app:
; src/webapp/core.clj
(ns webapp.core
(:require [compojure.core :refer [defroutes GET]]
[ring.middleware.json :as mid-json]
[clj-time.jdbc]))
(defn foo [request]
{:body {:now (org.joda.time.DateTime/now)}})
(defroutes routes
(GET "/foo" [] foo))
(def app
(-> routes
(mid-json/wrap-json-response)))
Hitting the /foo endpoint gives me this error:
com.fasterxml.jackson.core.JsonGenerationException: Cannot JSON encode object of class: class org.joda.time.DateTime: 2017-10-21T03:38:16.207Z
Is there a simple way to get ring-json to encode the DateTime object? Do I have to write my own middleware to convert it to e.g. a string first? If so, how would I do that? (I've never written ring middleware before).
My project.clj has these dependencies FYI:
[[org.clojure/clojure "1.8.0"]
[org.clojure/java.jdbc "0.6.1"]
[ring/ring-jetty-adapter "1.4.0"]
[compojure "1.4.0"]
[ring/ring-json "0.4.0"]
[clj-time "0.14.0"]]
If you're using Cheshire to generate JSON, you can extend its protocol to handle serialization then it should "just work":
(extend-protocol cheshire.generate/JSONable
org.joda.time.DateTime
(to-json [dt gen]
(cheshire.generate/write-string gen (str dt))))
Additionally, the following modification made it work for projects that are using the time library tick.alpha.api. I was getting the error
#error {:cause Cannot JSON encode object of class:
class java.time.Instant: 2019-10-23T00:31:40.668Z
:via
[{:type com.fasterxml.jackson.core.JsonGenerationException
:message Cannot JSON encode object of class:
class java.time.Instant: 2019-10-23T00:31:40.668Z
:at [cheshire.generate$generate invokeStatic generate.clj 152]}]
Implementing the following in the file db/core.clj fixed the issue for me.
(extend-protocol cheshire.generate/JSONable
java.time.Instant
(to-json [dt gen]
(cheshire.generate/write-string gen (str dt))))

Execute function once element is shown

In a reagent app created using luminus via
lein new luminus asdf +cljs
How can I execute a function once an element, say :div.container in the snippet below, has been shown?
(defn about-page []
[:div.container
[:div.row
[:div.col-md-12
"this is the story of asdf... work in progress"]]])
I just read the friendly manual :) and found that the luminus generated app is no different from standard reagent.
Changing above about-page function as follows does the trick. about-page-rendered is
(defn about-page-render []
[:div.container
[:div.row
[:div.col-md-12
"this is the story of asdf... work in progress"]]])
(defn about-page-rendered []
(do-what-ever-is-necessary))
(defn about-page []
(r/create-class {:reagent-render about-page-render
:component-did-mount about-page-rendered}))

How to get the selflink in a compojure handler?

When defining a compojure handler e.g. by using the defroutes macro, I can do something like this:
(defroutes home-routes
(GET "/myhome/:id" [ id ] (home-page)))
(defn home-page [ id ]
( ... do something ... ))
So I know how to pass a piece of the path parameter. But imagine I want to return a HAL+JSON object with a selflink. How would I get defroutes to pass the whole URI to the home-page function?
The Ring request map contains all the necessary information to construct a "selflink". Specifically, :scheme, :server-name, :server-port, and :uri values can be assembled into full request URL. When I faced this problem I created Ring middleware that adds the assembled request URL to the Ring request map. I could then use the request URL in my handlers as long as I pass the request map (or some subset of it) into the handler. The following snippet shows one way of implementing this:
(defroutes app-routes
(GET "/myhome/:id" [id :as {:keys [self-link]}] (home-page id self-link))
(route/resources "/")
(route/not-found "Not Found"))
(defn wrap-request-add-self-link [handler]
(fn add-self-link [{:keys [scheme server-name server-port uri] :as r}]
(let [link (str (name scheme) "://" server-name ":" server-port uri)]
(handler (assoc r :self-link link)))))
(def app
(-> app-routes
handler/site
wrap-request-add-self-link))

troubleshooting clojure web-app: connecting html and css for heroku deployment

I have two files, one html and one css. I have tried to turn them into a heroku app and even used the lein command to create a heroku friendly skeleton and plug these two files in, but cannot get it to work for the life of me. There is something very basic that I don't yet understand about how to coordinate a view with the back-end control. And the hello world tutorials aren't helping me because they do not show me how to do different things or explain what needs to change in my defroutes function, for example, for that to be accomplished. In short, my question is this: How can I coordinate these two files into a Clojure project to make the html render as the front page of a webapp and then deploy it on heroku?
html:
<html>
<head>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<img id="sun" src="http://goo.gl/dEEssP">
<div id='earth-orbit'>
<img id="earth" src="http://goo.gl/o3YWu9">
</div>
</body>
</html>
web.clj file in "lein new heroku ..." project:
(ns solar_system.web
(:require [compojure.core :refer [defroutes GET PUT POST DELETE ANY]]
[compojure.handler :refer [site]]
[compojure.route :as route]
[clojure.java.io :as io]
[ring.middleware.stacktrace :as trace]
[ring.middleware.session :as session]
[ring.middleware.session.cookie :as cookie]
[ring.adapter.jetty :as jetty]
[ring.middleware.basic-authentication :as basic]
[cemerick.drawbridge :as drawbridge]
[environ.core :refer [env]]))
(defn- authenticated? [user pass]
;; TODO: heroku config:add REPL_USER=[...] REPL_PASSWORD=[...]
(= [user pass] [(env :repl-user false) (env :repl-password false)]))
(def ^:private drawbridge
(-> (drawbridge/ring-handler)
(session/wrap-session)
(basic/wrap-basic-authentication authenticated?)))
(defroutes app
(ANY "/repl" {:as req}
(drawbridge req))
(GET "/" []
{:status 200
:headers {"Content-Type" "text/plain"}
:body (pr-str ["Hello" :from 'Heroku])}) ; <= Should I change this part here?
(ANY "*" []
(route/not-found (slurp (io/resource "404.html")))))
(defn wrap-error-page [handler]
(fn [req]
(try (handler req)
(catch Exception e
{:status 500
:headers {"Content-Type" "text/html"}
:body (slurp (io/resource "500.html"))}))))
(defn -main [& [port]]
(let [port (Integer. (or port (env :port) 5000))
;; TODO: heroku config:add SESSION_SECRET=$RANDOM_16_CHARS
store (cookie/cookie-store {:key (env :session-secret)})]
(jetty/run-jetty (-> #'app
((if (env :production)
wrap-error-page
trace/wrap-stacktrace))
(site {:session {:store store}}))
{:port port :join? false})))
;; For interactive development:
;; (.stop server)
;; (def server (-main))
project.clj file
(defproject solar_system "1.0.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://solar_system.herokuapp.com"
:license {:name "FIXME: choose"
:url "http://example.com/FIXME"}
:dependencies [[org.clojure/clojure "1.4.0"]
[compojure "1.1.1"]
[ring/ring-jetty-adapter "1.1.0"]
[ring/ring-devel "1.1.0"]
[ring-basic-authentication "1.0.1"]
[environ "0.2.1"]
[com.cemerick/drawbridge "0.0.6"]]
:min-lein-version "2.0.0"
:plugins [[environ/environ.lein "0.2.1"]]
:hooks [environ.leiningen.hooks]
:profiles {:production {:env {:production true}}})
example of typical handler code that renders text:
(ns hello-world.core
(:use ring.adapter.jetty))
(defn app [req]
{:status 200
:headers {"Content-Type" "text/plain"}
:body "Hello, world"}) ; <= Could I just change this part to slurp in
; the html file and stick it in a file in my
; root directory to get a successful 'git push heroku master'?
Modifying your code:
(defroutes app
(ANY "/repl" {:as req}
(drawbridge req))
(GET "/" []
{:status 200
:headers {"Content-Type" "text/html"} ; change content type
:body (slurp "resources/public/my-file.html")}) ; wherever your file is
(ANY "*" []
(route/not-found (slurp (io/resource "404.html")))))
How I'd write it:
(defroutes app
(ANY "/repl" {:as req} (drawbridge req))
(GET "/" [] (slurp "resources/public/my-file.html")) ; wherever your file is
(route/resources "/") ; special route for serving static files like css
; default root directory is resources/public/
(route/not-found (slurp (io/resource "404.html")))) ; IDK what io/resource does
; you might not need it