Changing an HTML Element in clojurescript - clojurescript

I can get an HTML element from my clojurescript page in the REPL:
cljs.user=> (.-innerHTML (.getElementById js/document "app"))
"This is clojure"
But how do i change this element to for example:
"This is awesome clojure"

You can mutate this element like so...
(set! (.-innerHTML (.getElementById js/document "app")) "This is awesome ClojureScript")
And please be aware that you could also say...
(set! (.. js/document (getElementById "app") -innerHTML) "This is awesome ClojureScript")
...in case that is more attractive to you. Or you could say...
(->> "This is awesome ClojureScript" (set! (.. js/document (getElementById "app") -innerHTML)))
...or...
(let [app-element (js/document.getElementById "app")] (set! (.-innerHTML app-element) "This is awesome ClojureScript"))
Blessings,
Raphael

Related

Problem opening files with the FileReader API

This has been bugging me for days. I have a web app that lets the user open documents from their local machine. I'm using the FileReader API for the first time.
It works correctly except for one use case.
Open a document file.
Programmatically create a new document, overwriting the existing one.
Open the same file as above.
When this sequence is executed, the second attempt fails silently (except that the file is not loaded).
Here is an example Reagent program (created from the figwheel-main template) that illustrates the problem.
(ns a-bad-button.core
(:require [reagent.core :as r]))
(def app-state-ratom (r/atom nil))
(defn new-doc []
{:doc-text "Some MINIMAL text to play with."})
(defn add-new-button
[aps]
(fn [aps]
[:input.tree-demo--button
{:type "button"
:value "New"
:on-click #(reset! aps (new-doc))}]))
(defn load-doc-data!
[aps file-data]
(swap! aps assoc :doc-text file-data))
(defn handle-file-open-selection
[aps evt]
(let [js-file-reader (js/FileReader.)]
(set! (.-onload js-file-reader)
(fn [evt] (load-doc-data! aps (-> evt .-target .-result))))
(.readAsText js-file-reader (aget (.-files (.-target evt)) 0))))
(defn add-open-button
[aps]
(fn [aps]
[:div
[:input {:type "file" :id "file-open-id"
:style {:display "none"}
:on-change #(handle-file-open-selection aps %)}]
[:input {:type "button"
:value "Open"
:on-click #(.click (.getElementById js/document "file-open-id"))}]]))
(defn a-bad-button
[aps]
(fn [aps]
[:div
[:h4 "A Bad Button"]
[:p#doc-text-p (or (:doc-text #aps) "Loaded text will go here.")]
[add-new-button aps]
[add-open-button aps]]))
(defn mount! [el]
(reset! app-state-ratom (new-doc))
(r/render-component [a-bad-button app-state-ratom] el))
(defn mount-app-element []
(when-let [el (.getElementById js/document "app")]
(mount! el)))
(mount-app-element)
(defn ^:after-load on-reload []
(mount-app-element))
With println debugging messages, it appears that execution reaches the :on-click handler in the add-open-button function, but the handler, handle-file-open-selection, is never reached or executed.
The failure occurs on Safari, Opera, Brave, and Vivaldi browsers. Files open as expected on Firefox.
Has anyone seen this before and fixed it?
Similar questions:
Filereader - upload same file again not working
FileReader onload not getting fired when selecting same file in Chrome
Basically, the problem is that onChange will not trigger when selecting the same file. One workaround is to set the value of the file input before the file browser opens to something like "", to always trigger an onChange event. In your case, it could look like changing your handle-file-open-selection function to:
(defn handle-file-open-selection
[aps evt]
(let [js-file-reader (js/FileReader.)]
(set! (.-onload js-file-reader)
(fn [evt]
(load-doc-data! aps (-> evt .-target .-result))))
(.readAsText js-file-reader (aget (.-files (.-target evt)) 0))
;; add this
(set! (.-value (.getElementById js/document "file-open-id")) "")
))

How to define default component in Reagent

I am trying to create SPA application with Reagent. How I can define default page/component? As I understand it, I should save current page in state atom. But I can't define it if I change state in home page.
For example, in this code home cannot be resolved (row 1):
(defonce app-state (atom {:current-page (home)}))
(defn second-page []
[:p 2])
(defn home []
[:div
[:p 1]
[:input {:type "button" :value "Click!"
:on-click #(swap! app-state second-page)}]])
(defn hello-world []
[:div
[:input {:type "button" :value "Home" :on-click #(swap! app-state home)}]
(:current-page #app-state)
[:h3 "footer"]])
(reagent/render-component [hello-world]
(. js/document (getElementById "app")))
There are a few ways you can solve this issue. One approach would be to create a map called pages like so
(def pages {:home home
:second second-page
....}
and then use the key name in the state atom i.e.
(defonce app-state (atom {:current-page :home})
Because you are just storing a keyword, there is no need for forward delarations as keywords evaluate to themselves. Then in your code, you would use the value for :current-page to lookup the compoonent from the pages map
defn hello-world []
[:div
[:input {:type "button" :value "Home" :on-click #(swap! app-state home)}]
((:current-page #app-state) pages)
[:h3 "footer"]])
You may also find things like secretary useful as an example of one way to generalise this sort of idea.
Just use declare to put placeholders for home and second-page before the atom:
(declare home second-page)
(defonce app-state (atom {:current-page home}))
Be sure to always keep a browser tab open to The Clojure CheatSheet. See also this answer for more details on the workings of Clojure's var feature.

How to hide/show a table in ClojureScript

I would like to show/hide a table when a font-awesome chevron button is clicked.
The following code comes from http://jsfiddle.net/z0y0hp8o/6/. I would like to do the same thing, but in clojurescript using java interop.
(document).on('click', '.panel-heading span.clickable', function(e){
var $this = $(this);
if(!$this.hasClass('panel-collapsed')) {
$this.parents('.panel').find('.specialCollapse').slideUp();
$this.addClass('panel-collapsed');
$this.find('i').removeClass('glyphicon-chevron-up').addClass('glyphicon-chevron-down');
} else {
$this.parents('.panel').find('.specialCollapse').slideDown();
$this.removeClass('panel-collapsed');
$this.find('i').removeClass('glyphicon-chevron-down').addClass('glyphicon-chevron-up');
} })
Here is my draft attempt in clojurescript.
(if (-> e -target -value
(.getElementsByClassName js/document "panel-collapsed"))
(do
(.slideUp js/document
(.find js/document ".specialCollapse"
(.parentElements js/document ".panel")))
(.addClass "panel-collapsed")
(.addClass "fas fa-chevron-down"
(.removeClass "fas fa-chevron-up"
(.find "i"))))
(do
(.slideDown js/document
(.find js/document ".specialCollapse"
(.parentElements js/document ".panel")))
(.removeClass "panel-collapsed")
(.addClass "fas fa-chevron-up"
(.removeClass "fas fa-chevron-down"
(.find "i")))))
I think you were really close.
I gave this a try with the following: I created a new project with Figwheel, using Leiningen like this: lein new figwheel jq-inter
In the new jq-inter folder, I edited the file in resources/public/index.html to have more/less the same contents of the rendered HTML file from the JSFiddle. Basically I copied the source of http://fiddle.jshell.net/z0y0hp8o/6/show/ removing the native JS version of the code between lines 64 and 84, and removed the block of JS used for messaging at the bottom of the file that is only required for JSFiddle themselves.
Right before the closing </body> tag, I added the line to the JS file what will be compiled from ClojureScript:
<script src="js/compiled/jq_inter.js" type="text/javascript"></script>
Now I edited the ClojureScript file at src/jq_inter/core.cljs so that it looks like this:
(ns jq-inter.core
(:require ))
(enable-console-print!)
(println "Loaded!")
(defonce $ js/$)
(-> ($ js/document)
(.on "click" ".panel-heading span.clickable"
(fn [e]
(let [$this ($ (-> e .-target))]
;; (js/console.log $this) ;; To check using the inspector
(if-not (.hasClass $this "panel-collapsed")
(do
(-> $this (.parents ".panel") (.find ".specialCollapse") (.slideUp))
(-> $this (.addClass "panel-collapsed"))
(-> $this (.removeClass "glyphicon-chevron-up") (.addClass "glyphicon-chevron-down")))
(do
(-> $this (.parents ".panel") (.find ".specialCollapse") (.slideDown))
(-> $this (.removeClass "panel-collapsed"))
(-> $this (.removeClass "glyphicon-chevron-down") (.addClass "glyphicon-chevron-up")))
)))))
(defn on-js-reload []
;; optionally touch your app-state to force rerendering depending on
;; your application
;; (swap! app-state update-in [:__figwheel_counter] inc)
)
You can start the project with lein figwheel. It will download a bunch of dependencies and start the compiler in the background, then serve the files from the resources folder. It took me a few tries to get the chevron icon working and in the end I think it was that the target of the click even was the <i> tag itself rather than the <span> for some reason. YMMV.
Found something that worked for me. I already had Bulma installed in my project, and was able to utilize a Bulma dropdown feature, with the following function from derHowie.
(defn toggle-class [id toggled-class]
(let [el-classList (.-classList (.getElementById js/document id))]
(if (.contains el-classList toggled-class)
(.remove el-classList toggled-class)
(.add el-classList toggled-class))))
implemented like
[:div {:class "dropdown-trigger"}
[:button {:class "button" :aria-haspopup "true" :aria-controls "drop-down-menu"
:on-click #(toggle-class "table-dropdown" "is-active")}
[:span "Table"]
[:span {:class "icon is-small"}
[:i {:class "fas fa-angle-down" :aria-hidden "true"}]]]]
[:div {:class "dropdown-menu" :id "dropdown-menu" :role "menu"}
[:div.dropdown-content
[:div {:class "dropdown-item"}
[table-to-be-displayed]]]]]
Pretty much, you can call the function toggle-class and send it the id of the element that you wish to modify, and the "is-active" class, and if that class is already applied to an element, it toggles it off, and then back on again. #Dennis provided a nice way to toggle the chevron up/down icon as well.

How to override onload methods in ClojureScript?

I am trying to override onload function of document and Image in ClojureScript. I think that set! should be possible to do it, but i am not getting any success. Relevant code is as follows :
(defn load-image [img-path]
(let [img (js/Image.)]
(do (set! (.-src img) img-path)
img)))
(defn add-img-canvas [img-path width height]
(let [img (load-image img-path)]
(set! (.-onload img)
(fn [] ;; This function is never called.
(let [canvas (get-scaled-canvas img width height)]
(do (pr-str canvas)
(swap! game-state :canvas canvas)))))))
(defn hello-world []
(let [count (atom 1)]
(fn []
[:div
[:h1 (:text #game-state)]
[:div (do (swap! count inc) (str "count is " #count))]
[:canvas (:canvas #game-state)]])))
(reagent/render-component [hello-world]
(. js/document (getElementById "app")))
(set! (.-onload js/document)
(fn [] ;; This function is also never called.
(add-img-canvas (:img-src game-state) 100 130)))
;;(. js/document onload)
Anonymous functions in add-img-canvas is not getting called. What am i doing wrong ?
I think it may be down to the difference between document.onload vs window.onload. The latter does work as expected.
See this for more details between the two.

How to create Material UI component in Om Clojurescript?

First of all, this https://github.com/taylorSando/om-material-ui doesn't work with latest React/Material UI.
The main reason, I think, is this warning in console:
Warning: Something is calling a React component directly. Use a factory or JSX instead. See: https://fb.me/react-legacyfactory
I've also tried to create component "manually":
(ns om-test.core
(:require [om.core :as om :include-macros true]
[om-tools.dom :as dom :include-macros true]
[om-tools.core :refer-macros [defcomponent]]
[om-material-ui.core :as mui :include-macros true]))
(enable-console-print!)
(defonce app-state (atom {:text "Hello Chestnut!"}))
(defn main []
(om/root
(fn [app owner]
(reify
om/IRender
(render [_]
(dom/div (dom/element js/MaterialUI.Paper {} "Hello")
(mui/paper {} "Hello"))
)))
app-state
{:target (. js/document (getElementById "app"))}))
So, both of these approaches produces same warning above.
There has been obviously some changes with React. It suggests to create components programatically as:
var React = require('react');
var MyComponent = React.createFactory(require('MyComponent'));
function render() {
return MyComponent({ foo: 'bar' });
}
So how do I create Material UI component inside Om render function, or maybe better How do I create React component inside Om render function, in general?
By Material UI I mean this https://github.com/callemall/material-ui
My dependencies
:dependencies [[org.clojure/clojure "1.6.0"]
[org.clojure/clojurescript "0.0-3058" :scope "provided"]
[ring "1.3.2"]
[ring/ring-defaults "0.1.4"]
[compojure "1.3.2"]
[enlive "1.1.6"]
[org.omcljs/om "0.9.0"]
[environ "1.0.0"]
[http-kit "2.1.19"]
[prismatic/om-tools "0.3.11"]
[om-material-ui "0.1.1" :exclusions [org.clojure/clojurescript
org.clojure/clojure]]]
Okay I eventually figured out.
Build latest version of Material UI with this: https://github.com/taylorSando/om-material-ui/tree/master/build-mui. Note: No need to build CSS in current version (0.10.4)
Include built material.js into your HTML file. Again, no need to include CSS.
Avoid loading React twice https://github.com/taylorSando/om-material-ui#avoid-loading-react-twice
Now the code for Om:
(ns material-ui-test.core
(:require [om.core :as om :include-macros true]
[om.dom :as dom :include-macros true]))
(enable-console-print!)
(defonce app-state (atom {:text "Hello Chestnut!"}))
(def ^:dynamic *mui-theme*
(.getCurrentTheme (js/MaterialUI.Styles.ThemeManager.)))
(defn main []
(om/root
(fn [app owner]
(reify
om/IRender
(render [_]
(let [ctor (js/React.createFactory
(js/React.createClass
#js
{:getDisplayName (fn [] "muiroot-context")
:childContextTypes #js {:muiTheme js/React.PropTypes.object}
:getChildContext (fn [] #js {:muiTheme *mui-theme*})
:render (fn []
(dom/div nil
(dom/h1 nil (:text app))
(js/React.createElement js/MaterialUI.Slider)))}))]
(ctor. nil)))))
app-state
{:target (. js/document (getElementById "app"))}))
If you used just (js/React.createElement js/MaterialUI.Slider) without :getChildContext etc. it would throw error:
Uncaught TypeError: Cannot read property 'component' of undefined
This is because of how current MaterialUI works. Read "Usage" part here: http://material-ui.com/#/customization/themes
Code for Reagent is bit more elegant. But I've used here namespace
[material-ui.core :as ui :include-macros true]
copy-pasted from this example project: https://github.com/tuhlmann/reagent-material
(def ^:dynamic *mui-theme*
(.getCurrentTheme (js/MaterialUI.Styles.ThemeManager.)))
(defn main-panel []
(let [active-panel (rf/subscribe [:active-panel])]
(r/create-class
{:display-name "Main Panel"
:child-context-types
#js {:muiTheme js/React.PropTypes.object}
:get-child-context
(fn [this]
#js {:muiTheme *mui-theme*})
:reagent-render
(fn []
[ui/Slider {:name "slide1"}])})))
EDIT: I released library, which greatly simplifies whole process.
Library: https://github.com/madvas/cljs-react-material-ui
Example app: https://github.com/madvas/cljs-react-material-ui-example
I'm not using Material UI but React Widgets. Here is the wrapper I needed to write for om:
(defn dropdown-list
[data owner {:keys [val-key menu-key id-key label-key props]}]
(reify
om/IRender
(render [_]
(let [menu (-get data menu-key)]
(js/React.createElement js/ReactWidgets.DropdownList
(-> {:defaultValue (-> (find-by-key menu id-key (-get data val-key))
(-get label-key))
:data (mapv #(-get % label-key) menu)
:onChange (fn [new-val]
(let [new-id (-> (find-by-key menu label- key new-val)
(-get id-key))]
(om/update! data val-key new-id)))}
(merge props)
clj->js))))))
So, in general, you need to get the React class (js/ReactWidgets.DropdownList) and call js/Readt.createElement while passing the props on render.