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.
Related
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
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")) "")
))
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 do you set a button to a particular width? This is one of the things I have tried so far:
(:require [om.next :as om :refer-macros [defui]]
[om.dom :as dom])
(defui HelloWorld
Object
(render [this]
(dom/button #js {:style {:width 300}} (get (om/props this) :title))))
Setting the title of the button works fine and is probably not relevant for this question. I've left it in because it is a typical thing to be doing, and placement of the attributes might be important.
The lein project.clj file has these dependencies:
[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.7.170"]
[org.omcljs/om "1.0.0-alpha24"]
[figwheel-sidecar "0.5.0-SNAPSHOT" :scope "test"]
I think the problem is due to #js only working on the top level. #JS will work on a top level map {} or vector [], but if you have nested data as values, you need to include additional #js calls for each embedded object.
What you really need is
(:require [om.next :as om :refer-macros [defui]]
[om.dom :as dom])
(defui HelloWorld
Object
(render [this]
(dom/button #js {:style #js {:width 300}} (get (om/props this) :title))))
Have a look at this post on using #js. For readability, rather than nested #js calls, you are often better off using clj->js
I got it to work with this:
(defui HelloWorld
Object
(render [this]
(dom/button (clj->js {:style {:width 300}}) (get (om/props this) :title))))
Note the use of clj->js.
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.