Interactive list in Reagent - clojure

I want to create a list of html elements (which include the results of a query) which are hidden by default but the user can toggle that state. I have tried a couple different ways below as toy examples but can't get either to work.
This code correctly creates three buttons, which were alter the exps state correctly but which do not ever hide content.
(:require [reagent.core :as r] )
(def exps (r/atom [true true true]))
(defn like-component []
[:div
(for [ [i r] (map-indexed vector ["A" "B" "C"])]
[:div
[:button {:on-click #(swap! exps update-in [i] not)}]
(when (nth #exps i)
[:pre (str i r)])])])
(r/render [like-component]
(js/document.getElementById "app"))
On the other hand, the code below will create only one element but it works correctly.
(defn expandable-view [e bool]
(let [expanded (r/atom bool)]
(fn []
[:li
[:div.expandable
[:div.header {:on-click #(swap! expanded not)}
"Click me to expand and collapse"]
(if #expanded
[:div.body (allow-html :pre e)])]])))
(defn like-component []
[:ul
(vec
(for [ e ["A" "B" "C"]]
(expandable-view e true ))) ])
(r/render [like-component]
(js/document.getElementById "app"))
Edit: Possibly related:
https://github.com/reagent-project/reagent/wiki/Beware-Event-Handlers-Returning-False

for is lazy, so reagent can't tell you're dereferencing exps in the first code snippet.
We can workaround it by explicitly dereferencing atoms.
(defn like-component []
(apply str #exps) ;; because #exps is a vector, and reagent
;; treat vectors as hiccup component
;; we can't just put `#exps` here.
[:div
(for [ [i r] (map-indexed vector ["A" "B" "C"])]
[:div
[:button {:on-click #(swap! exps update-in [i] not)}]
(when (nth #exps i)
[:pre (str i r)])])])
Or just wrap the lazy sequence in doall.
(defn like-component []
[:div
(doall (for [ [i r] (map-indexed vector ["A" "B" "C"])]
[:div
[:button {:on-click #(swap! exps update-in [i] not)}]
(when (nth #exps i)
[:pre (str i r)])]))])
FYI, related discussions.
any idea why the second block I posted only creates one element?
Vectors are special citizens for Reagent, they're treated as hiccup/React components.
For a working example
(defn like-component []
[:ul
(doall
(for [ e ["A" "B" "C"]]
[expandable-view e true]))])
Also notice that we are using [expandable-view e true] to properly construct a reagent component.
For more informations, I'd strongly suggest reading Using [] instead of () and Creating Reagent Components.

I achieved this type of behaviour by using bootstrap. I have a list of records in a state atom which are all hash maps. I add a :visible key to each map which is used to set the appropriate bootstrap classes on the record. There is a function which toggles the :visible setting in the has map. The component renders the record with a button which toggles visibility by changing the :visible value in the map, causing the component to re-render.
(defn toggle-visibility [k h]
(let [new-v (if (= "show" (:visible h))
"hidden"
"show")]
(state/set-value-in! [(state/this-page) :host-list k :visible] new-v)))
(defn host-component [k]
(let [host (state/value-in [(state/this-page) :host-list k])]
^{:key k} [:div.panel.panel-default
[:div {:class "panel-heading show"}
[:div {:class (condp = (:status host)
"Active" "text-success"
"Inactive" "text-info"
"Unknown" "text-warning"
:else "text-danger")}
[:button {:type "button" :class "btn btn-default"
:aria-label "Expand"
:on-click #(toggle-visibility k host)}
[:span {:class (str "glyphicon "
(if (= "show" (:visible host))
"glyphicon-minus"
"glyphicon-plus"))}]]
[:strong " IPv4 Address: "] (:ipv4 host)
[:strong " Hostname: "] (:hostname host)
[:div.pull-right (str "Host ID: " (:host-id host))]]]
[:div {:class (str "panel-body " (:visible host))}
[:ul.list-group
[:li.list-group-item
[:strong "Host Status: "] (:status host)]
[:li.list-group-item
[:strong "MAC Address: "] (:mac host)]
[:li.list-group-item
[:strong "IPv6 Address: "] (:ipv6 host)]
[:li.list-group-item
[:strong "Operating System: "] (:os host)]
[:li.list-group-item
[:strong "DHCP Client: "] (:dhcp host)
[:strong " DNS Entry: "] (:dns host)
[:strong " Revers DNS Entry: "] (:reverse-dns host)]
[:li.list-group-item
[:strong "Host Type: "] (:host-type host)]
[:li.list-group-item
[:strong "Network Group: "]
(str (:network-group host) " / " (:subgroup-name host))]
[:li.list-group-item
[:strong "Managed By: "] (:management-group host)]
[:li.list-group-item
[:strong "Creation Date: "] (:created-dt host)]
[:li.list-group-item
[:strong "Last Modified Date: "] (:last-modified-dt host)]
[:li.list-group-item
[:strong "Last Seen Date: "] (:last-seen-dt host)]]]]))
Essentially, letting bootstrap handle the showing/hiding of the content, leaving the code to just toggle the visible/invisible state. The full code is on my github page at theophilusx/Arcis

Related

clojure-more readable way to write this function?

I have written this function to convert a vector of maps into string. There is a second map called field-name-to-columns which contains a mapping between the field-name and the actual name of columns in my database.
My goal is to get a string like in the example where if the key is not present in the field-name-to-columns be ignored. Plus I want to have “client.name DESC” as a default if the :sorting key is empty or missing or none of the field-names matches any key in field-name-to-columns.
(def field-name-to-columns {"name" "client.name"
"birthday" "client.birthday"
"last-name" "client.last-name"
"city" "client.city"})
(def request {:sorting [{:field-name "city" :desc true}
{:field-name "country" :desc true}
{:field-name "birthday" :desc false}]})
(defn request-to-string
"this function creates the sorting part of query"
[sorting]
(if (empty? sorting)
(str "client.name" "DESC")
(->> (filter some? (for [{:keys [field-name desc]} sorting]
(when (some? (field-name-to-columns field-name)) (str (field-name-to-columns field-name) (when desc " DESC")))))
(st/join ", "))))
(request-to-string (request :sorting))
=>"client.city DESC, client.birthday"
Any comments on how to write this function more readable would be highly appriciated
What you've written is very reasonable in my opinion. I'd just add some whitespace for a visual break and tidy up your null handling a bit: it's silly to put nulls into the result sequence and then filter them out, rather than producing only non-nil values.
(defn request-to-string [sorting]
(str/join ", "
(or (seq (for [{:keys [field-name desc]} sorting
:let [column (field-name-to-columns field-name)]
:when column]
(str column (when desc " DESC"))))
["client.name DESC"])))
I've also moved the str/join up front; this is a stylistic choice most people disagree with me about, but you asked for opinions. I just think it's nice to emphasize that part by putting it up front, since it's an important part of the process, rather than hiding it at the back and making a reader remember the ->> as they read through the body of the function.
I also prefer using or rather than if to choose defaults, but it's not especially beautiful here. I also considered (or (non-empty (join ...)) "client.name DESC"). You might prefer either of these options, or your own choice, but I thought you'd like to see alternate approaches.
Here is one idea, based on my favorite template project.
(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test)
(:require
[tupelo.string :as str]))
(def field-name->columns {"name" "client.name"
"birthday" "client.birthday"
"last-name" "client.last-name"
"city" "client.city"})
(defn field->string
[{:keys [field-name desc]}]
; give names to intermediate and/or temp values
(let [col-name (field-name->columns field-name)]
(when (some? col-name)
(str col-name
(when desc " DESC")))))
(defn request->string
"this function creates the sorting part of query"
[sorting]
; accept only valid input
(when-not sorting ; WAS: (str "client.name" "DESC")
(throw (ex-info "sorting array required, value=" {:sorting sorting})))
; give names to intermediate values
(let [fiels-strs (filter some?
(for [entry sorting]
(field->string entry)))
result (str/join ", " fiels-strs)]
result))
and unit tests
(verify
(is= (field->string {:field-name "city", :desc true}) "client.city DESC")
(is= (field->string {:field-name "country", :desc true}) nil)
(is= (field->string {:field-name "birthday", :desc false}) "client.birthday")
(let [sorting [{:field-name "city" :desc true}
{:field-name "country" :desc true}
{:field-name "birthday" :desc false}]]
(is= (spyx-pretty (request->string sorting))
"client.city DESC, client.birthday")))
I prefer the (->> (map ...) (filter ...)) pattern over the for macro:
(defn request-to-string [sorting]
(or (->> sorting
(map (fn [{:keys [field-name desc]}]
[(field-name-to-columns field-name)
(when desc " DESC")]))
(filter first)
(map #(apply str %))
(clojure.string/join ", ")
not-empty)
"client.name DESC"))

How to pass new props to state of acomponent in Reagent?

I have a component:
(defn inner-input [cljs_element activeEl title]
(let [form (atom title)]
(fn [cljs_element activeEl title]
[:input {:type "text"
:onChange #(reset! form (.. % -target -value))
:on-blur #(change-title cljs_element (.. % -target -value))
:style {:display (if (:active (:node cljs_element)) "block" "none")
:width (* (+ 1 (count #form)) 8)
:max-width 730
:min-width 170}
:value #form}])))
It is nested in other component:
(defn card-input [cljs_element activeEl]
(fn [cljs_element activeEl]
(let [title (:title (:node cljs_element))]
[:div
[inner-input cljs_element activeEl title]])))
When i type data to the input in the inner-input component, i need update the local state form. And when the outer component card-input updates i want to reset my form to new title prop from argument. How can i achieve that?
I tried put (reset! form title) between let and fn in the inner-input component but it will not help
You can use reagent/track! to listen to changes to title, and reagent/dispose to stop listening. You can alternatively use add-watch and remove-watch, but track is a more convenient syntax.
(defn inner-input [title]
(reagent/with-let
[form (reagent/atom #title)
watch (reagent/track! (fn [] (reset! form #title)))]
[:label
"Inner input"
[:input {:on-change (fn [e]
(reset! form (.. e -target -value)))
:on-blur (fn [e]
(reset! title (.. e -target -value)))
:value #form}]]
(finally
(reagent/dispose! watch))))
(defn card-input []
(reagent/with-let
[title (reagent/atom "hello")]
[:div
[:label "Title"
[:input {:on-change (fn [e]
(reset! title (.. e -target -value)))
:value #title}]]
[inner-input title]]))
Now if you type in the inner input it will only update the outer when you exit the input box, but changing the outer title will immediately change the inner one. Is that what you wanted?
But if you don't want to pass title as a ratom and have to pass it as a value, then instead you can compare it to the previous value to determine if it changed, and reset form only when it changes.
(when (not= #previous-title title)
(do (reset! previous-title title)
(reset! form title)))
This code can go in render seeing as it is safe to call when form changes... nothing will happen.

reagent forms multi-select not working for list

I was following the sample code from http://yogthos.github.io/reagent-forms-example.html and was attempting the use the multi-select option for a list.
(defn select-item [item]
(go
(reset! current-selection item)
(let [response (<! (check-for-response))]
(reset! current-response response)
(reset! past-response response))))
;;batch
(defn item-list []
[:div#items-list
[items-list-header]
[:ul.list-group.items {:field :multi-select :id :pick-a-few}
(if (pos? (count #items))
(doall (for [item #items]
^{:key (item "upc")}
[:li.list-group-item [:a {:class (set-item-class item) :on-click #(select-item item) :href "#"}
(item "description")]]))
[:li [:a "No Items For This Department"]])]])
(defn product-component []
[:div
[item-list]
[product-response]
;[bind-fields item-list items]
;[bind-fields item-list product-response]
])
Does anyone know why I am unable to multi-select? The logic in select-item will change, but I can't seem to see the multi-select in the UI
I've been messing around with bind-fields in my product component with no success.

how to get this working in webnoir

I am trying to do this in webnoir.
This works:
(defpage [:post "/testurl] {:keys [name phone]}
(html5
(str "name: " name)
(str "phone: " phone)))
Now I want to generate defpages for many modules, each has a list of different fields. And I want to call the defpages from a function. The defpage must accept post for the fields.
Basically I have this: (def fields1 ["Name" "Phone" "Email" "xyz"])
And I would like to pass this to defpage, instead of having to specify the keys manually.
The fields might change in the future and that's why I want my code to pick up the fields and create the defpages dynamically on server startup.
Is it possible?
Thank you for all your help!
You can do this with a macro:
(defmacro defpages [pages]
`(do
~#(map (fn [page]
`(~'defpage [:post ~(str "/" (page :name))]
{:keys ~(into [] (map symbol (page :fields)))}
(~'html5
~#(map (fn [field]
`(str ~(str field ": ")
~(symbol field)))
(page :fields))))) pages)))
(defpages [{:name "testurl"
:fields ["name" "phone"]}
{:name "user"
:fields ["age" "address"]}])

Number format exception in compojure

As total clojure noob, I am trying to start one small tutorial app, in order to get familiar with compojure. It's a small application which lets user add two numbers, and after clicking on button displays their sum on the other page. I followed instruction from Mark McGranaghan blog. Everything seems ok, until I try to get sum of two numbers I have entered, instead of getting result, I am redirected to the same page (so basically I am stuck on first step of this tutorial). After checking the code, it seems that NumberFormatException is triggered when input parsing takes place (for some reason). In all my tests, I have tried to input all kinds of number format , but with no success. Here is the simplest code version , for which author said should work (I have tried the latest version from github site- same scenario: NFE):
(ns adder.core
(:use compojure.core)
(:use hiccup.core)
(:use hiccup.page-helpers))
(defn view-layout [& content]
(html
(doctype :xhtml-strict)
(xhtml-tag "en"
[:head
[:meta {:http-equiv "Content-type"
:content "text/html; charset=utf-8"}]
[:title "adder"]]
[:body content])))
(defn view-input []
(view-layout
[:h2 "add two numbers"]
[:form {:method "post" :action "/"}
[:input.math {:type "text" :name "a"}] [:span.math " + "]
[:input.math {:type "text" :name "b"}] [:br]
[:input.action {:type "submit" :value "add"}]]))
(defn view-output [a b sum]
(view-layout
[:h2 "two numbers added"]
[:p.math a " + " b " = " sum]
[:a.action {:href "/"} "add more numbers"]))
(defn parse-input [a b] ;; this is the place where problem occures
[(Integer/parseInt a) (Integer/parseInt b)])
(defroutes app
(GET "/" []
(view-input))
(POST "/" [a b]
(let [[a b] (parse-input a b)
sum (+ a b)]
(view-output a b sum)))
Can anyone tell me better way to pars the input values, in order to avoid this exception?I have tried couple of techniques , but nothing worked for me. I am using Leningen v1.7.1 with clojure 1.3 on win 7 machine.
Here is content of my project.clj file:
(defproject adder "0.0.1"
:description "Add two numbers."
:dependencies
[[org.clojure/clojure "1.3.0"]
[org.clojure/clojure-contrib "1.1.0"]
[ring/ring-core "1.0.2"]
[ring/ring-devel "1.0.2"]
[ring/ring-jetty-adapter "1.0.2"]
[compojure "1.0.1"]
[hiccup "0.3.8"]]
:dev-dependencies
[[lein-run "1.0.0"]])
and run.clj script:
(use 'ring.adapter.jetty)
(require 'adder.core)
(let [port (Integer/parseInt (get (System/getenv) "PORT" "8080"))]
(run-jetty #'adder.core/app {:port port}))
Thanks.
You are using compojure 1.0.1, the example in the blog you are following is using compojure 0.4.0.
As of version 0.6.0, Compojure no longer adds default middleware to routes. This means you must explicitly add the wrap-params and wrap-cookies middleware to your routes.
Source: https://github.com/weavejester/compojure
So you need to explicitly add the wrap-params middleware. So the following changes are required...
(ns adder.core
(:use ; change to idiomatic usage of :use
[compojure.core]
[hiccup.core]
[hiccup.page-helpers]
[ring.middleware.params :only [wrap-params]])) ; add middleware for params
(defn view-layout [& content]
(html
(doctype :xhtml-strict)
(xhtml-tag "en"
[:head
[:meta {:http-equiv "Content-type"
:content "text/html; charset=utf-8"}]
[:title "adder"]]
[:body content])))
(defn view-input []
(view-layout
[:h2 "add two numbers"]
[:form {:method "post" :action "/"}
[:input.math {:type "text" :name "a" :id "a"}] [:span.math " + "]
[:input.math {:type "text" :name "b" :id "a"}] [:br]
[:input.action {:type "submit" :value "add"}]]))
(defn view-output [a b sum]
(view-layout
[:h2 "two numbers added"]
[:p.math a " + " b " = " sum]
[:a.action {:href "/"} "add more numbers"]))
(defn parse-input [a b]
[(Integer/parseInt a) (Integer/parseInt b)])
(defroutes main-routes ; needs to be renamed
(GET "/" []
(view-input))
(POST "/" [a b]
(let [[a b] (parse-input a b)
sum (+ a b)]
(view-output a b sum))))
(def app (wrap-params main-routes)) ; wrap the params to allow destructuring to work