I am trying to write a merge method to generate CSS styles dynamically. This method should take breakpoints, and styles param and creates a map which we use for styling using stylefy.
I am trying to do this using specter, but unable to get desired results.
The code I tried until now:
(defn merge-style
[breakpoints style]
(let [media-queries (s/transform [s/ALL] #(hash-map :min-width (str %1 "px")) breakpoints)]
breakpoints))
The method should work as follows:
(def breakpoints ["320px" "600px" "1280px"])
(def style {:padding-top ["20px" "30px" "40px" "50px"]
:margin "30px"
})
(merge-style breakpoints style)
The output should look like the following:
{:padding-top "20px"
:margin "30px"
::stylefy/media {{:min-width "320px"} {:padding-top "30px"}
{:min-width "600px"} {:padding-top "40px"}
{:min-width "1280px"} {:padding-top "50px"}}
}
SOLUTION:
I solved this problem for myself using the following function
(defn- get-media-queries
[breakpoints styles]
(let [base-style (s/transform [s/MAP-VALS] #(%1 0) styles)
styles-maps (s/setval [s/MAP-VALS empty?] s/NONE (s/setval [s/MAP-VALS s/FIRST] s/NONE styles))
styles-list (map (fn [[key val]] (map #(hash-map key %1) val)) styles-maps)
styles-final (apply vdu/merge-maps styles-list)
breaks (map #(hash-map :min-width %1) breakpoints)
styles-merged (into {} (mapv vector breaks styles-final))
]
(assoc base-style ::stylefy/media styles-merged)))
Thanks a lot for providing help.
No need for Spectre on this one. Just use basic Clojure:
(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test) )
(def desired
{:padding-top "20px"
:margin "30px"
:stylefy/media {{:min-width "320px"} {:padding-top "30px"}
{:min-width "600px"} {:padding-top "40px"}
{:min-width "1280px"} {:padding-top "50px"}}})
(def breakpoints [320 600 1280])
(def padding-top ["30px" "40px" "50px"])
(def base {:padding-top "20px"
:margin "30px"})
(dotest
(let [mw (for [it breakpoints]
{:min-width (str it "px")})
pt (for [it padding-top]
{:padding-top it})
pairs (zipmap mw pt)
result (assoc base :stylefy/media pairs)]
(is= desired result)))
Note that since I don't have a namespace alias for stylefy, I am only using the single-colon version of the keyword.
Update 2019-9-12
Was just revisiting this post, and noticed I should have used zipmap instead of zip, even though the previous version (accidentally!) got the right answer.
Related
I am currently using the two following blocks of code to access nested values in ClojureScript:
(def response (re-frame/subscribe [::subs/quote]))
(def body (:body #response))
(def value (:value body))
(println value)
(def result (-> #(re-frame/subscribe [::subs/quote]) :body :value))
(println result)
(def lol (get-in #(re-frame/subscribe [::subs/quote]) [:body :value]))
(println lol)
Are there any better / more succinct ways of doing this?
Keys can be used as operators to retrieve its value like so:
(def lol (:value (:body #(re-frame/subscribe [::subs/quote]))))
(println lol)
However, I prefer the verbose way using a function as get-in
I am trying to merge vector of maps.
I tried doing it using the reduce method but unable to retrieve the expected result.
(def data '([{:padding-top "30px"} {:padding-top "40px"} {:padding-top "50px"}] [{:margin "40px"}]))
(reduce #(hash-map %1 %2) () data)
Input data:
(def data '([{:padding-top "30px"} {:padding-top "40px"} {:padding-top "50px"}] [{:margin "40px"}]))
(defn merge-data
[data]
)
Expected Output:
(merge-data data)
({:padding-top "30px" :margin "40px"}
{:padding-top "40px"}
{:padding-top "50px"})
Coming from the JS background, I can easily do it using something like forEach and conditionals to build expected output. But how to do it in functional way?
SOLUTION:
I was able to solve this problem in the following way
(defn merge-styles
[& args]
(let [max-count (apply max (map #(count %1) args))
items (map #(take max-count (concat %1 (repeat nil))) args)]
(apply map merge items)))
The code snippet makes it much clearer and leaner.
Thanks a lot for all the answers which helped me get up to this point.
Generally, you can just use map and merge to merge collections of hashmaps, but it will end merging when one of the collections is exhausted.
You can create a function like the following to "extend" the collections to have the same length, then merge as usual:
(defn merge-all
"Merges two sequences of maps using merge. Consumes all entries."
[xs ys]
(let [n (max (count xs) (count ys))
xs (take n (concat xs (repeat nil)))
ys (take n (concat ys (repeat nil)))]
(map merge xs ys)))
(def data [[{:padding-top "30px"} {:padding-top "40px"} {:padding-top "50px"}]
[{:margin "40px"}]])
;; (apply merge-all data)
;; => ({:padding-top "30px", :margin "40px"} {:padding-top "40px"} {:padding-top "50px"})
Note that in your example, you used a parenthesis around the data, but in Clojure this means you want to call it as if it were a function. In the example above I switched it to a [ and ] instead. Also, note that this function depends in the fact that you can actually count the collections that you pass to it (in Clojure you can have "infinite" collections, such as (range)).
(map into [{:a 1} {:c 3}] (concat [{:b 2}] (repeat nil)))
yields
({:a 1, :b 2} {:c 3}).
map reads from both vectors and applies the respective indexes to into, merging the maps for each index. As map stops when one of the collections is exhausted, we need to concat nil values to the shorter collection.
Edit: As always, this has already been answered before: Using 'map' with different sized collections in clojure
I would break it down into multiple simple steps like so:
(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test))
(def data [[{:padding-top "30px"} {:padding-top "40px"} {:padding-top "50px"}]
[{:margin "40px"}]])
; be clear about the 2 sequences we are combining
(def data-1 (first data))
(def data-2 (second data))
(defn do-merge []
(let [N (max (count data-1) (count data-2))]
(vec (for [i (range N)]
(let [map-1 (get data-1 i)
map-2 (get data-2 i)] ; returns `nil` if no value present
(merge map-1 map-2) ; if merge a map & nil, it's a noop
)))))
(dotest
(is= (do-merge)
[{:padding-top "30px", :margin "40px"}
{:padding-top "40px"}
{:padding-top "50px"}] ) )
You could also pad the shorter sequence with nils, then use (map merge ...), but you would have to make sure you get the lengths just right first which would still involve count and max....not sure if that is simpler.
I'm trying to parse HTML with CSS into Hiccup in a Reagent project. I am using Hickory. When I parse HTML with inline CSS, React throws an exception.
(map
as-hiccup (parse-fragment "<div style='color:red'>test</div>")
)
The above generates [:div {:style color:red} "test"] & Reactjs returns exception from Reactjs:
Violation: The style prop expects a mapping from style properties to values, not a string.
I believe [:div {:style {"color" "red"}} "test"] must be returned instead.
Here is the code view:
(ns main.views.job
(:require [reagent.core :as reagent :refer [atom]]
[hickory.core :refer [as-hiccup parse parse-fragment]]))
(enable-console-print!)
(defn some-view [uid]
[:div
(map as-hiccup (parse-fragment "<div style='color:red'>test</div>"))
])
The whole repo is here and it works. I added the parsing from style tag to a map for React in the core.cljs file:
(ns hickory-stack.core
(:require [clojure.string :as s]
[clojure.walk :as w]
[reagent.core :as reagent :refer [atom]]
[hickory.core :as h]))
(enable-console-print!)
(defn string->tokens
"Takes a string with syles and parses it into properties and value tokens"
[style]
{:pre [(string? style)]
:post [(even? (count %))]}
(->> (s/split style #";")
(mapcat #(s/split % #":"))
(map s/trim)))
(defn tokens->map
"Takes a seq of tokens with the properties (even) and their values (odd)
and returns a map of {properties values}"
[tokens]
{:pre [(even? (count tokens))]
:post [(map? %)]}
(zipmap (keep-indexed #(if (even? %1) %2) tokens)
(keep-indexed #(if (odd? %1) %2) tokens)))
(defn style->map
"Takes an inline style attribute stirng and converts it to a React Style map"
[style]
(tokens->map (string->tokens style)))
(defn hiccup->sablono
"Transforms a style inline attribute into a style map for React"
[coll]
(w/postwalk
(fn [x]
(if (map? x)
(update-in x [:style] style->map)
x))
coll))
;; Test Data
(def good-style "color:red;background:black; font-style: normal ;font-size : 20px")
(def html-fragment
(str "<div style='" good-style "'><div id='a' class='btn' style='font-size:30px;color:white'>test1</div>test2</div>"))
;; Rendering
(defn some-view []
[:div (hiccup->sablono
(first (map h/as-hiccup (h/parse-fragment html-fragment))))])
(reagent/render-component [some-view]
(. js/document (getElementById "app")))
Now can use compojure this way:
(GET ["/uri"] [para1 para2]
)
Para1 and para2 are all of type String.
I would like to let it know the type correcttly,like this:
(GET ["/uri"] [^String para1 ^Integer para2]
)
It can convert para1 to be Sting and para2 to Integer.
Is there some library or good way to do this?
This is possible as of Compojure 1.4.0 using the syntax [x :<< as-int]
This is not currently possible with only Compojure.
You could use Prismatic schema coercion.
(require '[schema.core :as s])
(require '[schema.coerce :as c])
(require '[compojure.core :refer :all])
(require '[ring.middleware.params :as rparams])
(def data {:para1 s/Str :para2 s/Int s/Any s/Any})
(def data-coercer (c/coercer data c/string-coercion-matcher ))
(def get-uri
(GET "/uri" r
(let [{:keys [para1 para2]} (data-coercer (:params r))]
(pr-str {:k1 para1 :k2 (inc para2)}))))
(def get-uri-wrapped
(let [keywordizer (fn [h]
(fn [r]
(h (update-in r [:params] #(clojure.walk/keywordize-keys %)))))]
(-> get-uri keywordizer rparams/wrap-params)))
Here is a sample run:
(get-uri-wrapped {:uri "/uri" :query-string "para1=a¶2=3" :request-method :get})
{:status 200,
:headers {"Content-Type" "text/html; charset=utf-8"},
:body "{:k1 \"a\", :k2 4}"}
I want to create a function that allows me to pull contents from some feed, here's what I have... zf is from here
(:require
[clojure.zip :as z]
[clojure.data.zip.xml :only (attr text xml->)]
[clojure.xml :as xml ]
[clojure.contrib.zip-filter.xml :as zf]
)
(def data-url "http://api.eventful.com/rest/events/search?app_key=4H4Vff4PdrTGp3vV&keywords=music&location=Belgrade&date=Future")
(defn zipp [data] (z/xml-zip data))
(defn contents[cont & tags]
(assert (= (zf/xml-> (zipp(parsing cont)) (seq tags) text))))
but when I call it
(contents data-url :events :event :title)
I get an error
java.lang.RuntimeException: java.lang.ClassCastException: clojure.lang.ArraySeq cannot be cast to clojure.lang.IFn (NO_SOURCE_FILE:0)
(Updated in response to the comments: see end of answer for ready-made function parameterized by the tags to match.)
The following extracts the titles from the XML pointed at by the URL from the question text (tested at a Clojure 1.5.1 REPL with clojure.data.xml 0.0.7 and clojure.data.zip 0.1.1):
(require '[clojure.zip :as zip]
'[clojure.data.xml :as xml]
'[clojure.data.zip.xml :as xz]
'[clojure.java.io :as io])
(def data-url "http://api.eventful.com/rest/events/search?app_key=4H4Vff4PdrTGp3vV&keywords=music&location=Belgrade&date=Future")
(def data (-> data-url io/reader xml/parse))
(def z (zip/xml-zip data))
(mapcat (comp :content zip/node)
(xz/xml-> z
(xz/tag= :events)
(xz/tag= :event)
(xz/tag= :title)))
;; value of the above right now:
("Belgrade Early Music Festival, Gosta / Purcell: Dido & Aeneas"
"Belgrade Early Music Festival, Gosta / Purcell: Dido & Aeneas"
"Belgrade Early Music Festival, Gosta / Purcell: Dido & Aeneas"
"VIII Early Music Festival, Belgrade 2013"
"Kevlar Bikini"
"U-Recken - Tree of Life Pre event"
"Green Day"
"Smallman - Vrane Kamene (Crows Of Stone)"
"One Direction"
"One Direction in Serbia")
Some comments:
The clojure.contrib.* namespaces are all deprecated. xml-> now lives in clojure.data.zip.xml.
xml-> accepts a zip loc and a bunch of "predicates"; in this context, however, the word "predicate" has an unusual meaning of a filtering function working on zip locs. See clojure.data.zip.xml source for several functions which return such predicates; for an example of use, see above.
If you want to define a list of predicates separately, you can do that too, then use xml-> with apply:
(def loc-preds [(xz/tag= :events) (xz/tag= :event) (xz/tag= :title)])
(mapcat (comp :content zip/node) (apply xz/xml-> z loc-preds))
;; value returned as above
Update: Here's a function which takes the url and keywords naming tags as arguments and returns the content found at the tags:
(defn get-content-from-tags [url & tags]
(mapcat (comp :content zip/node)
(apply xz/xml->
(-> url io/reader xml/parse zip/xml-zip)
(for [t tags]
(xz/tag= t)))))
Calling it like so:
(get-content-from-tags data-url :events :event :title)
gives the same result as the mapcat form above.