clojure.data.json write/read affects enlive data - json

What is the appropriate json way to save and reload enlive's html-resource outputs.
The following procedure does not preserve the data structure (note that I ask json/read-str to map keys to symbols):
(require net.cgrand.enlive-html :as html)
(require clojure.data.json :as json)
(def craig-home
(html/html-resource (java.net.URL. "http://www.craigslist.org/about/sites")))
(spit "./data/test_json_flow.json" (json/write-str craig-home))
(def craig-reloaded
(json/read-str (slurp "./data/test_json_flow.json") :key-fn keyword))
(defn count-nodes [page] (count (html/select page [:div.box :h4])))
(println (count-nodes craig-home)) ;; => 140
(println (count-nodes craig-reloaded)) ;; => 0
Thanks.
UPDATE
To address Mark Fischer's comment I post a different code that address html/select instead of html/html-resource
(def craig-home
(html/html-resource (java.net.URL. "http://www.craigslist.org/about/sites")))
(def craig-boxes (html/select craig-home [:div.box]))
(count (html/select craig-boxes [:h4])) ;; => 140
(spit "./data/test_json_flow.json" (json/write-str craig-boxes))
(def craig-boxes-reloaded
(json/read-str (slurp "./data/test_json_flow.json") :key-fn keyword))
(count (html/select craig-boxes-reloaded [:h4])) ;; => 0

A simpler approach would be to write/read using Clojure edn:
(require '[net.cgrand.enlive-html :as html])
(require '[clojure.data.json :as json])
(def craig-home (html/html-resource (java.net.URL. "http://www.craigslist.org/about/sites")))
(spit "./data/test_json_flow.json" (pr-str craig-home))
(def craig-reloaded
(clojure.edn/read-string (slurp "./data/test_json_flow.json")))
(defn count-nodes [page] (count (html/select page [:div.box :h4])))
(println (count-nodes craig-home)) ;=>140
(println (count-nodes craig-reloaded)) ;=>140
Enlive expects the tag name value also to be a keyword and will not find a node if the tag name value is a string (which is what json/write-str and json/read-str converts keywords to).
(json/write-str '({:tag :h4, :attrs nil, :content ("Illinois")}))
;=> "[{\"tag\":\"h4,\",\"attrs\":null,\"content\":[\"Illinois\"]}]"
(json/read-str (json/write-str '({:tag :h4, :attrs nil, :content ("Illinois")})) :key-fn keyword)
;=> [{:tag "h4", :attrs nil, :content ["Illinois"]}]
(pr-str '({:tag :h4 :attrs nil :content ("Illinois")}))
;=> "({:tag :h4, :attrs nil, :content (\"Illinois\")})"
(clojure.edn/read-string (pr-str '({:tag :h4, :attrs nil, :content ("Illinois")})))
;=> ({:tag :h4, :attrs nil, :content ("Illinois")})
If you must use json then you can use the following to convert the :tag values to keywords:
(clojure.walk/postwalk #(if-let [v (and (map? %) (:tag %))]
(assoc % :tag (keyword v)) %)
craig-reloaded)

Related

Get all of named routes

I am currently learning reagent with secretary as its route. I find that I can use query-params to get a hash-map of all parameters with question mark (?) like ?name=Daniel
(ns feampersanda.core
(:require-macros [secretary.core :refer [defroute]])
(:import goog.History)
(:require
[secretary.core :as secretary]
[goog.events :as events]
[goog.history.EventType :as EventType]
[reagent.core :as r]))
;; ------------------------------
;; States
;; page --> is occupied by page state
(def app-state (r/atom {:params {}}))
;; ------------------------------
;; History
(defn hook-browser-navigation! []
(doto (History.)
(events/listen
EventType/NAVIGATE
(fn [event]
(secretary/dispatch! (.-token event))))
(.setEnabled true)))
;; -------------------------
;; Views
;; -------------------------
;; Parameters
(defn update-query-params! [query-params]
(do
(js/console.log (str query-params))
(swap! app-state assoc-in [:params :query] query-params))
)
;; -------------------------
;; Routing Config
(defn app-routes []
(secretary/set-config! :prefix "#")
(defroute "/" [query-params]
(do
(update-query-params! query-params)
(swap! app-state assoc :page :home)))
(defroute "/about/:id" [id query-params]
(do
(js/console.log id)
(update-query-params! query-params)
(swap! app-state assoc :page :about)))
(hook-browser-navigation!))
(defmulti current-page #(#app-state :page))
(defmethod current-page :home []
[:div [:h1 (str "Home Page")]
[:a {:href "#/about"} "about page"]
[:br]
[:a {:href "#/about"} (str (:count #app-state))]
])
(defmethod current-page :about []
[:div [:h1 "About Page"]
[:a {:href "#/"} (str "home page" " --> "
(:yes (:query (:params #app-state)))
)]])
(defmethod current-page :default []
[:div
[:p "404"]
])
;; -------------------------
;; Initialize app
(defn mount-root []
(app-routes)
(r/render [current-page] (.getElementById js/document "app")))
(defn init! []
(mount-root))
I don't know how to pass the id parameter to a defmethod, so I want it to be saved inside an atom, so I wonder how to get hash-map which is include all of the named parameters like http://0.0.0.0:3449/#/about/black/12 to {:path "black" :id "12"}
One solution would be to use cemerick's URL library
(require '[cemerick.url :as url])
(keys (:query (url/url (-> js/window .-location .-href))))
https://github.com/cemerick/url

Om Next subquery doesn't have effect on sub component props

Reading this Om Next tutorial page Components, Identity & Normalization, I thought the subquery from the subcomponent (Person component) is used to populate the Person's props. But changing the query from
'[:name :points :age]
to
'[]
doesn't break the app. Could you help me understand how the parser invokes read methods from these component/query tree.
The entire code from the page is below.
(def init-data
{:list/one [{:name "John" :points 0}
{:name "Mary" :points 0}
{:name "Bob" :points 0}]
:list/two [{:name "Mary" :points 0 :age 27}
{:name "Gwen" :points 0}
{:name "Jeff" :points 0}]})
;; -----------------------------------------------------------------------------
;; Parsing
(defmulti read om/dispatch)
(defn get-people [state key]
(let [st #state]
(into [] (map #(get-in st %)) (get st key))))
(defmethod read :list/one
[{:keys [state] :as env} key params]
{:value (get-people state key)})
(defmethod read :list/two
[{:keys [state] :as env} key params]
{:value (get-people state key)})
(defmulti mutate om/dispatch)
(defmethod mutate 'points/increment
[{:keys [state]} _ {:keys [name]}]
{:action
(fn []
(swap! state update-in
[:person/by-name name :points]
inc))})
(defmethod mutate 'points/decrement
[{:keys [state]} _ {:keys [name]}]
{:action
(fn []
(swap! state update-in
[:person/by-name name :points]
#(let [n (dec %)] (if (neg? n) 0 n))))})
;; -----------------------------------------------------------------------------
;; Components
(defui Person
static om/Ident
(ident [this {:keys [name]}]
[:person/by-name name])
static om/IQuery
(query [this]
'[])
Object
(render [this]
(println "Render Person" (-> this om/props :name))
(let [{:keys [points name age] :as props} (om/props this)]
(dom/li nil
(dom/label nil (str name ", points: " points ", age: " age))
(dom/button
#js {:onClick
(fn [e]
(om/transact! this
`[(points/increment ~props)]))}
"+")
(dom/button
#js {:onClick
(fn [e]
(om/transact! this
`[(points/decrement ~props)]))}
"-")))))
(def person (om/factory Person {:keyfn :name}))
(defui ListView
Object
(render [this]
(println "Render ListView" (-> this om/path first))
(let [list (om/props this)]
(apply dom/ul nil
(map person list)))))
(def list-view (om/factory ListView))
(defui RootView
static om/IQuery
(query [this]
(let [subquery (om/get-query Person)]
`[{:list/one ~subquery} {:list/two ~subquery}]))
Object
(render [this]
(println "Render RootView")
(let [{:keys [list/one list/two]} (om/props this)]
(apply dom/div nil
[(dom/h2 nil "List A")
(list-view one)
(dom/h2 nil "List B")
(list-view two)]))))
(def reconciler
(om/reconciler
{:state init-data
:parser (om/parser {:read read :mutate mutate})}))
(om/add-root! reconciler
RootView (gdom/getElement "app"))
From reading this now I know that only the top level queries are processed by the parser. And you are responsible for providing your own reads for subqueries by accessing (:query env).

om.next mutate of other-component state not causing other-component to re-render

I am updating state in one of my mutations, and a piece of it is not used by this component, but is by another one. When I do the mutate I see the that the app-state is updated in the repl, and if I cause the component to re-render for other reasons, it will show correctly, but I can not get the mutate to schedule a re-render of the second component. In the example below clicking on a button should decrement the value near the color name in the second list, but it does not.
There is some examples showing using :value [k k] in the mutate return, but those throw an error, must be out of date tutorials, as the current format is :value {:keys [...]}, so says the code and some tutorials . However I can't find any part of om.next actually USING :keys as a keyword that isn't a destructure operation (so not using :keys as an actual keyword, but it is a common word so I may have missed one somewhere)
In the repl I see this for the app-state:
=> (om/app-state reconciler)
#object [cljs.core.Atom {:val
{:tiles [[:tile/by-pos "a7"]
[:tile/by-pos "a9"]
[:tile/by-pos "a11"]],
:inventory [[:inv/by-color "red"]
[:inv/by-color "blue"]
[:inv/by-color "green"]],
:tile/by-pos {"a7" {:pos "a7", :color nil},
"a9" {:pos "a9", :color nil},
"a11" {:pos "a11", :color nil}},
:inv/by-color {"red" {:color "red", :remaining 2},
"blue" {:color "blue", :remaining 1},
"green" {:color "green", :remaining 1}}}}]
What am I missing?
(ns omnexttest.core
(:require [goog.dom :as gdom]
[om.next :as om :refer-macros [defui]]
[om.dom :as dom]))
(defmulti read om/dispatch)
(defmethod read :default
[{:keys [state] :as env} key params]
(let [st #state ]
(if-let [[_ value] (find st key)]
{:value value}
{:value :not-found})))
(defmethod read :tiles
[{:keys [state] :as env} key params]
{:value (into [] (map #(get-in #state %) (get #state key))) })
(defmethod read :inventory
[{:keys [state] :as env} key params]
{:value (into [] (map #(get-in #state %) (get #state key))) })
(defmulti mutate om/dispatch)
(defmethod mutate 'draw/edit-edge
[{:keys [state] :as env} _ {:keys [this pos color]}]
{:value {:keys [[:inv/by-color color :remaining]]}
:action (fn [] (do
(swap! state assoc-in [:tile/by-pos pos :color] color )
(swap! state update-in [:inv/by-color color :remaining] dec)))})
(defn hex-color
[ this pos color ]
(om/transact! this `[(draw/edit-edge ~{:this this :pos pos :color color})]))
(defui TileView
static om/Ident
(ident [this {:keys [pos]}] [:tile/by-pos pos])
static om/IQuery
(query [this] '[:pos :color])
Object
(render [this]
(let [{:keys [pos color] :as props} (om/props this)]
(dom/li nil
(str pos " " color)
(for [color ["red" "green" "blue"]]
(dom/button #js { :onClick (fn [e] (hex-color this pos color)) }
color))))))
(def tile-view (om/factory TileView {:keyfn :pos}))
(defui InvView
static om/Ident
(ident [this {:keys [color]}] [:inv/by-color color])
static om/IQuery
(query [this] '[:color :remaining])
Object
(render [this]
(let [{:keys [color remaining] :as props} (om/props this) ]
(dom/li nil (str color " " remaining)))))
(def inv-view (om/factory InvView {:keyfn :color}))
(def app-state {
:tiles [{ :pos "a7" :color nil }
{ :pos "a9" :color nil }
{ :pos "a11" :color nil }
]
:inventory [{ :color "red" :remaining 2}
{ :color "blue" :remaining 1}
{ :color "green" :remaining 1}]
})
(defui MapView
static om/IQuery
(query [this]
[{:tiles (om/get-query TileView)}
{:inventory (om/get-query InvView) }])
Object
(render [this]
(let [tiles (-> this om/props :tiles)
inv (-> this om/props :inventory) ]
(dom/div nil
(dom/ul nil
(mapv tile-view tiles))
(dom/ul nil
(mapv inv-view inv))))))
(def reconciler
(om/reconciler
{:state app-state
:parser (om/parser {:read read :mutate mutate})}))
(om/add-root! reconciler
MapView (gdom/getElement "map"))
(defn on-js-reload []
;; optionally touch your app-state to force rerendering depending on
;; your application
;; (swap! app-state update-in [:__figwheel_counter] inc)
)
The this that is passed into om/transact! is important for re-rendering, so here if this was for a MapView component then all three components would be re-rendered. You can have the function in MapView (thus using MapView's this) but call it from TileView. In TileView's render you need something like this:
{:keys [click-cb-fn]} (om/get-computed this)
When you call om/transact! re-rendering is done down from the component you pass as first argument - this. Thus, to take this to its extreme, you'll never have re-rendering problems if all om/transacts!s are done from the root component, and all functions are passed down via computed props.
But you don't have to pass functions down. An alternative is to keep them at the same component where the firing button is, and instead pass down (again via computed props) the parent component's this. All that matters is what component the first argument to om/transact! is - call om/transact! from where ever you like.
Follow on reads are another thing to be considered when thinking about re-rendering, but not for the example you gave - they are best considered when the component you need to be re-rendered is in a different subbranch of the render tree, where using a common root's this would not be practical.
Another thing to note is that a mutate's value is 'just for documentation'. So whatever you put there will have no effect.

Parse HTML and build a map from the parsed value using clojure

I am using enlive clojure to parse HTML. My parser looks like;
(def each-rows
(for [tr crawl-page
:let [row (html/select tr [:td (attr= :class "bl_12")])]
:when (seq row)]
row))
which extracts result as following;
{:tag :a,
:attrs
{:class "bl_12",
:href
"url1"},
:content ("Chapter 1")}
{:tag :a,
:attrs
{:class "bl_12",
:href
"url2"},
:content ("Chapter 2")}
{:tag :a,
:attrs
{:class "bl_12",
:href
"url3"},
:content ("Chapter 3")}
Now my objective is to get a dictionary like this;
{:Chapter_1 "url1"
:Chapter_2 "url2"
:Chapter_3 "url3"}
I managed to write a method which extracts only href or only content, but couldn't make it as a map
(defn read-specific-other [x]
(map (comp second :attrs) x))
output : [:href "url1"]
(defn read-specific-content [x]
(map (comp first ::content) x))
(map read-specific-content each-rows)
output :
(("Chapter 1"
"Chapter 2"
"Chapter 3"
))
How do I get the desired result
Take a look at zipmap
(zipmap (read-specific-other each-rows) (read-specific-content each-rows))
If you really want the keys to be keywords, then use the keyword function; but I recommend keeping strings as the keys.
Also consider using an into for pattern instead:
(into {}
(for [[{:keys [attrs]} {:keys [content]}] rows]
[content attrs]))

Cannot manipulate cursor outside of render phase

Trying react for the first time, and I want to make a simple todo list app. But every time I press enter to trigger onSubmit it says Uncaught Error: Cannot manipulate cursor outside of render phase, only om.core/transact!, om.core/update!, and cljs.core/deref operations allowed. While I think this is a very good error message, I don't know what to do.
(ns app.core
(:require [om.core :as om :include-macros true]
[sablono.core :as html :refer-macros [html]]))
(def app-state (atom
{:todos [{:todo "first"}
{:todo "second"}]
:current ""}))
(defn to-do
[data]
(om/component
(html [:li (:todo data)])))
(defn to-dos
[data]
(om/component
(html [:div
[:form {:on-submit (fn [e]
(.preventDefault e)
(om/transact! data :todos (fn [v]
(js/console.log (:current data))
(conj v (:current data)))))}
[:input {:type "text"
:placeholder "Enter some text."
:on-change (fn [e] (om/update! data :current (.. e -target -value)))}]]
[:ul
(om/build-all to-do (:todos data))]])))
(om/root to-dos app-state {:target js/document.body})
I think the problem is where you access data inside om/transact! where you should operate on v:
(:current v) instead of (:current data)
or you may try (:current #data) for most recent value of data
There are actually two issues with:
(om/transact! data :todos (fn [v]
(js/console.log (:current data))
(conj v (:current data)))))
One is what #edbond said above: you should use (:current v) rather than (:current data). The other problem, however, is that you are specifying the :todos keyword, and instead, you should simply change data itself, since :current is outside of :todos in your app-state shown. So the correct formulation would be:
(om/transact! data (fn [v]
(js/console.log (:current v))
(conj v (:current v)))))