├── src ├── main │ ├── clj-kondo.exports │ │ └── dv │ │ │ └── cljs-emotion │ │ │ └── config.edn │ └── dv │ │ ├── emotion_helix_dom.cljc │ │ ├── emotion_css.cljc │ │ ├── cljs_emotion_reagent.cljc │ │ └── cljs_emotion.cljc └── dev │ └── dv │ └── cljs_emotion │ ├── fulcro_cards.cljs │ ├── reagent_debug.cljs │ ├── debug.cljs │ ├── helix_cards.cljs │ ├── target_styled.cljs │ ├── reagent_cards.cljs │ └── devcards.cljs ├── test └── dv │ └── cljs_emotion_test.clj ├── .gitignore ├── resources └── public │ └── devcards.html ├── package.json ├── Makefile ├── shadow-cljs.edn ├── LICENSE ├── music.md ├── deps.edn ├── pom.xml ├── README.md └── yarn.lock /src/main/clj-kondo.exports/dv/cljs-emotion/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as 2 | {dv.cljs-emotion/defstyled clojure.core/def 3 | dv.cljs-emotion-reagent/defstyled clojure.core/def}} 4 | -------------------------------------------------------------------------------- /test/dv/cljs_emotion_test.clj: -------------------------------------------------------------------------------- 1 | (ns dv.cljs-emotion-test 2 | (:require [clojure.test :refer :all] 3 | [dv.cljs-emotion :refer :all])) 4 | 5 | (deftest a-test 6 | (testing "FIXME, I fail." 7 | (is (= 0 1)))) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | *.jar 5 | *.class 6 | /.cpcache 7 | /.lein-* 8 | /.nrepl-history 9 | /.nrepl-port 10 | *.iml 11 | *.idea 12 | *.shadow-cljs 13 | node_modules 14 | yarn-error.log 15 | resources/public/js 16 | -------------------------------------------------------------------------------- /resources/public/devcards.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | dv.cljs-emotion devcards 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dv.cljs-emotion", 3 | "version": "1.2.3", 4 | "repository": "git@github.com:dvingo/cljs-emotion.git", 5 | "author": "Daniel Vingo ", 6 | "license": "MIT", 7 | "devDependencies": { 8 | "@emotion/react": "^11.10.5", 9 | "@emotion/styled": "^11.10.5", 10 | "create-react-class": "^15.7.0", 11 | "highlight.js": "^10.4.1", 12 | "marked": "^1.1.0", 13 | "polished": "^4.1.3", 14 | "react": "^17.0.2", 15 | "react-dom": "^17.0.2", 16 | "shadow-cljs": "^2.16.10" 17 | }, 18 | "dependencies": { 19 | "react-grid-layout": "^0.16.6" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := bash 2 | .ONESHELL: 3 | .SHELLFLAGS := -eu -o pipefail -c 4 | .DELETE_ON_ERROR: 5 | MAKEFLAGS += --warn-undefined-variables 6 | MAKEFLAGS += --no-builtin-rules 7 | 8 | ifeq ($(origin .RECIPEPREFIX), undefined) 9 | $(error This Make does not support .RECIPEPREFIX. Please use GNU Make 4.0 or later) 10 | endif 11 | .RECIPEPREFIX = > 12 | 13 | jar: 14 | > clojure -Spom 15 | > clojure -X:jar 16 | 17 | dev: 18 | > yarn 19 | > yarn shadow-cljs watch devcards 20 | 21 | # To deploy to gh-pages: 22 | 23 | # Make commits on master 24 | # gco gh-pages 25 | # git merge master 26 | # make gh-pages 27 | # gc -m 28 | # gpoh -f 29 | gh-pages: 30 | > yarn shadow-cljs release devcards 31 | > mv resources/public/js/devcards/devcards.js docs/ 32 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:deps 2 | {:aliases [:dev]} 3 | 4 | :nrepl 5 | {:port 3434} 6 | 7 | :builds 8 | {:devcards {:target :browser 9 | :output-dir "resources/public/js/devcards" 10 | :asset-path "/js/devcards" 11 | 12 | :compiler-options {:devcards true 13 | :closure-defines {dv.cljs-emotion/ADD_CLASSNAMES true 14 | dv.cljs-emotion-reagent/ADD_CLASSNAMES true}} 15 | 16 | :modules {:devcards {:entries [dv.cljs-emotion.devcards] 17 | :init-fn dv.cljs-emotion.devcards/main}} 18 | 19 | :devtools {:http-port 4001 20 | :http-root "resources/public" 21 | :push-state/index "devcards.html" 22 | :after-load dv.cljs-emotion.devcards/main}}}} 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Daniel Vingo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /music.md: -------------------------------------------------------------------------------- 1 | 2 | This file contains links to music listened to while creating this software. 3 | 4 | Bonobo - Essential Mix 1481 (Live at Glastonbury) - 02 July 2022 | BBC Radio 1 5 | 6 | https://www.youtube.com/watch?v=PwbYUFY6Q3A 7 | 8 | Maya Jane Coles Boiler Room & Ballantine's True Music Russia DJ Set 9 | 10 | https://www.youtube.com/watch?v=DZuXhbpuRRY 11 | 12 | Maya Jane Coles Boiler Room & Ballantine's Stay True Scotland DJ Set 13 | 14 | https://www.youtube.com/watch?v=7m0v5uOQ0XQ 15 | 16 | Waterkant Souvenirs Podcast Mira 2012-12-14 17 | 18 | https://soundcloud.com/mira_kater/waterkant_podcast?in=serjin/sets/mira-chris 19 | 20 | 21 | Mira | mach mal langsam... | www.klangextase.de 22 | 23 | https://soundcloud.com/mira_kater/mira-mach-mal-langsam-klangextase?in=serjin/sets/mira-chris 24 | 25 | 26 | Tara Brooks 27 | 28 | https://soundcloud.com/deep-house-amsterdam/tara-brooks-dha-mix-318 29 | 30 | 31 | nicolás jaar 2 hour mix april 16 19:00 gmt 32 | 33 | https://soundcloud.com/prcvl/nicolas-jaar-april161900gmt 34 | 35 | 36 | massive attack essential mix bbc radio 1 1994-11-12 37 | https://www.youtube.com/watch?v=qhmdED0Nq4c 38 | 39 | 40 | Nina Kraviz - Billie Eilish - Hans Zimmer ◆ You Can't Stop Me (Electro Junkiee Mix) 41 | 42 | https://www.youtube.com/watch?v=CR0fDrij2E8 43 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/main" "resources"] 2 | :deps {org.clojure/clojure {:mvn/version "1.10.3"} 3 | camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"} 4 | borkdude/dynaload {:mvn/version "0.3.5"}} 5 | :aliases 6 | {:dev {:extra-paths ["src/dev"] 7 | :extra-deps {devcards/devcards {:mvn/version "0.2.7"} 8 | reagent/reagent {:mvn/version "1.1.1" :exclusions [cljsjs/react]} 9 | com.fulcrologic/fulcro {:mvn/version "3.5.29"} 10 | lilactown/helix {:mvn/version "0.1.9"} 11 | sablono/sablono {:mvn/version "0.8.6"} 12 | binaryage/devtools {:mvn/version "1.0.6"} 13 | dv/clj-utils {:mvn/version "2021-10-31.0.0"} 14 | thheller/shadow-cljs {:mvn/version "2.16.10"}}} 15 | :test {:extra-paths ["test"] 16 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}}} 17 | :runner 18 | {:extra-deps {com.cognitect/test-runner 19 | {:git/url "https://github.com/cognitect-labs/test-runner" 20 | :sha "dd6da11611eeb87f08780a30ac8ea6012d4c05ce"}} 21 | :main-opts ["-m" "cognitect.test-runner" "-d" "test"]} 22 | 23 | :jar {:replace-deps {seancorfield/depstar {:mvn/version "2.0.165"}} 24 | :exec-fn hf.depstar/jar 25 | :exec-args {:jar "dv.cljs-emotion.jar"}} 26 | 27 | :install {:extra-deps {deps-deploy/deps-deploy {:mvn/version "0.0.12"}} 28 | :main-opts ["-m" "deps-deploy.deps-deploy" "install" "dv.cljs-emotion.jar"]} 29 | 30 | :deploy {:extra-deps {deps-deploy/deps-deploy {:mvn/version "0.0.12"}} 31 | :main-opts ["-m" "deps-deploy.deps-deploy" "deploy" "dv.cljs-emotion.jar"]}}} 32 | -------------------------------------------------------------------------------- /src/dev/dv/cljs_emotion/fulcro_cards.cljs: -------------------------------------------------------------------------------- 1 | (ns dv.cljs-emotion.fulcro-cards 2 | (:require 3 | [com.fulcrologic.fulcro.rendering.multiple-roots-renderer :as fr] 4 | [devcards.core :as dc :refer (defcard)] 5 | [dv.cljs-emotion.target-styled] 6 | [dv.cljs-emotion.reagent-cards] 7 | [dv.devcards-fulcro3 :as f3] 8 | [com.fulcrologic.fulcro.dom :as dom] 9 | [com.fulcrologic.fulcro.components :as fc :refer [defsc]] 10 | [sablono.core :as sab :refer [html]] 11 | ["polished" :as p :refer [darken]] 12 | [dv.cljs-emotion :as em :refer [defstyled keyframes global-style theme-provider]])) 13 | 14 | (defsc Button [this {:keys [className text] :as props}] 15 | {:query [:className :text] 16 | :ident (fn [_] [::id :button]) 17 | :initial-state (fn [_] {:text "Hello world"})} 18 | (dom/div 19 | (dom/button {:className className} text))) 20 | 21 | (def ui-button (fc/computed-factory Button)) 22 | 23 | (defonce app-id (random-uuid)) 24 | 25 | (def fulcro-app 26 | (f3/upsert-app 27 | {::f3/root Button 28 | ::f3/root-state {} 29 | ::f3/wrap-root? true 30 | ::f3/persistence-key app-id 31 | ::f3/app {} 32 | :fulcro.inspect.core/app-id app-id})) 33 | 34 | (comment 35 | (println "HI") 36 | 37 | (fr/with-app-context 38 | fulcro-app 39 | (ui-button {:className "TEST CLS"}))) 40 | 41 | (defcard dv-test-fulcro-styled-button 42 | (dc/dom-node 43 | (fn [_ dom-node] 44 | (f3/mount-at fulcro-app 45 | {::f3/root Button 46 | ::f3/wrap-root? true 47 | ::f3/persistence-key app-id} 48 | dom-node)))) 49 | 50 | ;(f3/make-card Button) 51 | 52 | ;; the thought is to do this: 53 | ;(defstyled ui-fulcro-btn Button {:color "red"}) 54 | 55 | ; and then figure out the best way to pass the className to the returned react-element before rendering - 56 | ;; probably in render middleware? 57 | ; and have this pass the className to fulcro 58 | -------------------------------------------------------------------------------- /src/dev/dv/cljs_emotion/reagent_debug.cljs: -------------------------------------------------------------------------------- 1 | (ns dv.cljs-emotion.reagent-debug 2 | (:require 3 | [devcards.core :as dc :refer [defcard-rg defcard]] 4 | ["polished" :as p :refer [darken]] 5 | [dv.cljs-emotion-reagent :as em :refer [defstyled keyframes global-style theme-provider]])) 6 | 7 | (defstyled full-height :div 8 | {:height "100%" 9 | :background "pink"} 10 | (fn [props] 11 | (.log js/console "PROPS: " props) 12 | (:styles props))) 13 | 14 | (defn nested-fn [] 15 | [:div 16 | (full-height 17 | {:styles {:width "70%" "@media (min-width: 700px)" {:background "skyBLUE" :width "100%"}}} 18 | [:p "hello body"])]) 19 | 20 | (dc/defcard-doc "Cljs properties should be passed to callbacks untouched." (dc/mkdn-pprint-source full-height)) 21 | 22 | (defcard breakpoint-debug (dc/mkdn-pprint-source nested-fn)) 23 | 24 | (defcard (dc/reagent [nested-fn])) 25 | 26 | (defcard 27 | "Testing 1-arity test." 28 | (dc/reagent 29 | [:div 30 | [full-height 31 | {:styles {":before" 32 | {:content "'HELLO'" 33 | "@media (min-width: 700px)" {:content "'LARGE SCREEN'"}} 34 | :width "70%" "@media (min-width: 700px)" {:background "skyBLUE" :width "100%"}}}]])) 35 | 36 | (defstyled switch-case :div 37 | (fn [{:keys [status] :as args}] 38 | ;(.log js/console "ARGS: " args) 39 | {:background 40 | (case status 41 | :active "seagreen" 42 | :offline "grey" 43 | :away "blue" 44 | "white")})) 45 | 46 | (defcard "Test keywords - you should get keywords as values when passed as keywords." 47 | (dc/reagent 48 | [:div 49 | [:div "Active" [switch-case {:status :active} "Should be green"]] 50 | [:div "Offline" [switch-case {:status :offline} "Should be grey"]] 51 | [:div "Away" [switch-case {:status :away} "Should be blue"]] 52 | [:div "Missing" [switch-case "Should be white"]]])) 53 | 54 | (defstyled hover-hand :div {":hover" {:cursor "pointer"}}) 55 | (defcard "Test js-props - hover should show pointer" 56 | (dc/reagent [:div [hover-hand #js{:key "login-label"} "Login"] ])) 57 | -------------------------------------------------------------------------------- /src/dev/dv/cljs_emotion/debug.cljs: -------------------------------------------------------------------------------- 1 | (ns dv.cljs-emotion.debug 2 | (:require 3 | [devcards.core :as dc :refer (defcard)] 4 | [sablono.core :as sab :refer [html]] 5 | ["polished" :as p :refer [darken]] 6 | [dv.cljs-emotion :as em :refer [defstyled keyframes global-style theme-provider]])) 7 | 8 | (defstyled full-height :div 9 | {:height "100%" 10 | :background "pink"} 11 | (fn [props] 12 | (.log js/console "PROPS: " props) 13 | (:styles props))) 14 | 15 | (defn nested-fn [] 16 | (html [:div 17 | (full-height 18 | {:styles {:width "70%" "@media (min-width: 700px)" {:background "skyBLUE" :width "100%"}}} 19 | (html [:p "hello body"]))])) 20 | 21 | (defcard "Cljs properties should be passed to callbacks untouched." (dc/mkdn-pprint-source full-height)) 22 | 23 | (defcard breakpoint-debug (dc/mkdn-pprint-source nested-fn)) 24 | 25 | (defcard (nested-fn)) 26 | 27 | (defcard 28 | "Testing 1-arity test." 29 | (html [:div 30 | (full-height 31 | {:styles {":before" 32 | {:content "'HELLO'" 33 | "@media (min-width: 700px)" {:content "'LARGE SCREEN'"}} 34 | :width "70%" "@media (min-width: 700px)" {:background "skyBLUE" :width "100%"}}})])) 35 | 36 | (defstyled switch-case :div 37 | (fn [{:keys [status] :as args}] 38 | ;(.log js/console "ARGS: " args) 39 | {:background 40 | (case status 41 | :active "seagreen" 42 | :offline "grey" 43 | :away "blue" 44 | "white")})) 45 | 46 | (defcard 47 | "# Test keywords 48 | You should get keywords as values in a style fn when passed as keywords." 49 | (html 50 | [:div 51 | [:div "Active" (switch-case {:status :active} "Should be green")] 52 | [:div "Offline" (switch-case {:status :offline} "Should be grey")] 53 | [:div "Away" (switch-case {:status :away} "Should be blue")] 54 | [:div "Missing" (switch-case "Should be white")]])) 55 | 56 | 57 | (defstyled hover-hand :div {":hover" {:cursor "pointer"}}) 58 | (defcard "Test js-props - hover should show pointer" 59 | (html [:div (hover-hand #js{:key "login-label"} "Login") ])) 60 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | dv 5 | cljs-emotion 6 | 2023-07-16.0.0 7 | cljs-emotion 8 | ClojureScript wrapper of emotion css-in-js library 9 | https://github.com/dvingo/dv.cljs-emotion 10 | 11 | 12 | MIT 13 | https://opensource.org/licenses/MIT 14 | 15 | 16 | 17 | 18 | Daniel Vingo 19 | 20 | 21 | 22 | https://github.com/dvingo/dv.cljs-emotion 23 | scm:git:git://github.com/dvingo/cljs-emotion.git 24 | scm:git:ssh://git@github.com/dvingo/cljs-emotion.git 25 | HEAD 26 | 27 | 28 | 29 | org.clojure 30 | clojure 31 | 1.10.3 32 | 33 | 34 | com.fulcrologic 35 | guardrails 36 | 1.1.9 37 | 38 | 39 | camel-snake-kebab 40 | camel-snake-kebab 41 | 0.4.2 42 | 43 | 44 | borkdude 45 | dynaload 46 | 0.2.2 47 | 48 | 49 | 50 | src/main 51 | 52 | 53 | 54 | clojars 55 | https://repo.clojars.org/ 56 | 57 | 58 | 59 | 60 | clojars 61 | Clojars repository 62 | https://clojars.org/repo 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/dev/dv/cljs_emotion/helix_cards.cljs: -------------------------------------------------------------------------------- 1 | (ns dv.cljs-emotion.helix-cards 2 | (:require 3 | [devcards.core :as dc :refer (defcard)] 4 | [dv.cljs-emotion.fulcro-cards] 5 | [dv.cljs-emotion.reagent-cards] 6 | [dv.cljs-emotion.reagent-debug] 7 | [dv.cljs-emotion.debug] 8 | [dv.cljs-emotion.target-styled] 9 | [dv.emotion-helix-dom :as d] 10 | [dv.cljs-emotion :as em] 11 | [helix.core :refer [$ defnc]] 12 | [dv.cljs-emotion :as em :refer [defstyled keyframes global-style theme-provider]])) 13 | 14 | (defcard "# Helix dom support for :css prop. 15 | 16 | The namespace `dv.emotion-helix-dom` is a copy of the `helix.dom` namespace with added support for emotion's `:css` property. 17 | 18 | 19 | If a component does not include the `:css` property and it does not include the dynamic property symbols `:&` and `&`, 20 | then helix's `dom-props` macro is expanded on the props, so there is no performance cost on top of helix for components 21 | that don't use styles. For any components using dynamic props the code is the same as that in helix with the addition 22 | of the `:css` property being wrapped to deal with cljs->js datatype conversion.") 23 | 24 | (defnc a-component [] 25 | (d/div {:css (fn [{:keys [bg]}] 26 | {:color "yellow" 27 | :background-color (or bg "black")})} 28 | "Here is some text in a div")) 29 | 30 | (defnc a-table [] 31 | (let [dyn-props {:class "TESTING" 32 | :style {:background-color "red"}}] 33 | (d/table {:css {:background-color "blue" 34 | "tbody > tr:nth-of-type(odd)" {:background-color "purple"} 35 | :border "2px solid yellow"} 36 | :& dyn-props} 37 | (d/tbody 38 | (d/tr (d/td "hello")) 39 | (d/tr (d/td "hello1")) 40 | (d/tr (d/td "hello2")) 41 | (d/tr (d/td "hello2")) 42 | (d/tr (d/td "hello2")) 43 | (d/tr (d/td "hello2")) 44 | (d/tr (d/td "hello2")) 45 | (d/tr (d/td "hello2")) 46 | (d/tr (d/td "hello2")))))) 47 | 48 | (defnc vector-of-styles [] 49 | (d/div {:css [{:color "black"} {:background-color "#efefef"} #js {:borderRadius "4px"} {:border "1px solid"}]} 50 | "This text should be black on #efefef")) 51 | 52 | (defcard helix-css 53 | (d/div 54 | (d/h1 "This is a helix table with the :css prop targeting the children rows") 55 | ($ a-table) 56 | ($ vector-of-styles))) 57 | 58 | (dc/defcard-doc "In the above example we're using nested selectors with both static props and dynamic 59 | (you can inspect this page to see the `TESTING` css class is applied). 60 | 61 | In the second example we're using a 62 | vector of styles which are merged together by emotion." 63 | (dc/mkdn-pprint-source a-table) 64 | (dc/mkdn-pprint-source vector-of-styles)) 65 | 66 | (defnc theme-component [] 67 | (d/div 68 | (em/theme-provider {:theme {:bg "cadetblue"}} 69 | (d/h2 "With theme, bg should be cadetblue") 70 | ($ a-component)) 71 | (d/h2 "With no theme, bg should be black") 72 | ($ a-component))) 73 | 74 | (defcard theme-support 75 | ($ theme-component)) 76 | 77 | (dc/defcard-doc 78 | "This example demonstrates using a theme and function as the value for `:css` to read the theme data." 79 | (dc/mkdn-pprint-source theme-component) 80 | (dc/mkdn-pprint-source a-component)) 81 | 82 | (def some-css 83 | {:self (em/css #js {:backgroundColor "#aefefe" 84 | :border "2px solid #eee" 85 | :borderRadius "0.5rem" 86 | :padding "1rem"}) 87 | :title (em/css {:font-size "1.25rem" 88 | :color "red"} 89 | #js{:border "1px dashed" 90 | "> div > p" {:color "blue"}}) 91 | :more (em/css {:border "2px solid"})}) 92 | 93 | (def merged-css 94 | (em/css 95 | {:background-color "black"} 96 | (:title some-css) 97 | (:more some-css))) 98 | 99 | (defn object-styles [] 100 | (d/div {:css (:self some-css)} 101 | (d/h2 {:css (:title some-css)} "here we have a title") 102 | (d/h2 {:css merged-css} "here we have a title") 103 | (d/p {:css (em/css merged-css {:color "white"})} 104 | "And overwrite inline"))) 105 | 106 | (defcard object-styles-card 107 | (object-styles)) 108 | 109 | (dc/defcard-doc 110 | "We can use object styles as described here [https://emotion.sh/docs/object-styles](https://emotion.sh/docs/object-styles) 111 | and here [https://emotion.sh/docs/composition](https://emotion.sh/docs/composition) 112 | 113 | We have the `css` function which lets us mix cljs datatypes with javascript ones. 114 | 115 | This is very flexible and allows creating a library of reusable styles which can be combined per component as needed." 116 | (dc/mkdn-pprint-source some-css) 117 | (dc/mkdn-pprint-source merged-css) 118 | (dc/mkdn-pprint-source object-styles)) 119 | 120 | 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ClojureScript wrapper for emotion 2 | 3 | [![Clojars Project](https://img.shields.io/clojars/v/dv/cljs-emotion.svg)](https://clojars.org/dv/cljs-emotion) 4 | 5 | [![cljdoc badge](https://cljdoc.org/badge/dv/cljs-emotion)](https://cljdoc.org/d/dv/cljs-emotion) 6 | 7 | 8 | Documentation and example usage are hosted in devcards here: 9 | 10 | https://dvingo.github.io/cljs-emotion/#!/dv.cljs_emotion.devcards 11 | 12 | https://dvingo.github.io/cljs-emotion 13 | 14 | For info about emotion itself, see: 15 | 16 | https://emotion.sh/docs/introduction 17 | 18 | # To use this library 19 | 20 | Include it in your clojure project dependency list (see most recent version on Clojars). 21 | 22 | And install emotion: 23 | 24 | ```bash 25 | npm install @emotion/react @emotion/styled 26 | ``` 27 | The main namespaces are: 28 | 29 | `dv.cljs-emotion` - Plain React components/elements 30 | 31 | With API: `defstyled`, `jsx`, `css`, `global-style`, `theme-provider`, `keyframes` 32 | 33 | `dv.cljs-emotion-reagent` - Reagent components 34 | 35 | With API: `defstyled`, `jsx`, `css`, `global-style`, `theme-provider`, `keyframes` 36 | 37 | `dv.emotion-helix-dom` 38 | 39 | DOM elements for use with helix that support the `:css` prop, with the same API as the components in the `helix.dom` namespace. 40 | 41 | ## Use with plain react components 42 | 43 | ```clojure 44 | (require [dv.cljs-emotion :refer [jsx css defstyled keyframes global-style theme-provider]]) 45 | 46 | (defstyled sample1 :div 47 | {:background-color "RebeccaPurple"}) 48 | 49 | (react/createElement sample1 nil "hello") 50 | ``` 51 | 52 | See the devcards for lots more details 53 | 54 | https://dvingo.github.io/cljs-emotion/#!/dv.cljs_emotion.devcards 55 | 56 | ## Use with helix 57 | 58 | https://github.com/lilactown/helix 59 | 60 | The namespace `dv.emotion-helix-dom` is a drop in replacement for `helix.dom`, and supports all the props that helix 61 | supports in addition to supporting the `:css` property of emotion. Here is an example: 62 | 63 | ```clojure 64 | (:require 65 | [dv.emotion-helix-dom :as d] 66 | [helix.core :refer [$ defnc]] 67 | [dv.cljs-emotion :as em :refer [css defstyled keyframes global-style theme-provider]] 68 | 69 | (def some-css 70 | {:self (em/css #js {:backgroundColor "#aefefe" 71 | :border "2px solid #eee" 72 | :borderRadius "0.5rem" 73 | :padding "1rem"}) 74 | :title (em/css {:font-size "1.25rem" 75 | :color "red"} 76 | #js{:border "1px dashed" 77 | "> div > p" {:color "blue"}}) 78 | :more (em/css {:border "2px solid"})}) 79 | 80 | (def merged-css 81 | (em/css 82 | {:background-color "black"} 83 | (:title some-css) 84 | (:more some-css))) 85 | 86 | (defn object-styles [] 87 | (d/div {:css (:self some-css)} 88 | (d/h2 {:css (:title some-css)} "here we have a title") 89 | (d/h2 {:css merged-css} "here we have a title") 90 | (d/p {:css (em/css merged-css {:color "white"})} 91 | "And overwrite inline"))) 92 | ``` 93 | 94 | You can of course use the `defstyled` API as well if you wish as that outputs React components. 95 | 96 | More examples in the devcards: 97 | 98 | https://dvingo.github.io/cljs-emotion/#!/dv.cljs_emotion.helix_cards 99 | 100 | ## Use with reagent 101 | 102 | There is a separate namespace for reagent support: 103 | 104 | ```clojure 105 | (:require 106 | [dv.cljs-emotion-reagent :refer [jsx css defstyled keyframes global-style theme-provider]]) 107 | 108 | (defstyled a-child :div {:color "deepSKYBlue"}) 109 | 110 | (defstyled a-parent2 :div 111 | (fn [{:keys [color]}] 112 | {:color "red" 113 | a-child {:color (or color "darkorchid")} 114 | "@media (min-width: 1024px)" 115 | {a-child {:color "black"}}})) 116 | 117 | (def black-bg {:background-color "black"}) 118 | 119 | ;; in your component's render function: 120 | 121 | [:div 122 | [a-parent2 "parent should be red"] 123 | (jsx {:css (em/css black-bg {:color "white"})}) 124 | [a-parent2 {:color "steelblue"} 125 | [a-child "nested child should be darkorchid"]]] 126 | ``` 127 | 128 | The component output by `defstyled` supports passing reagent vectors as children and will convert them to React Elements 129 | for you. 130 | 131 | If you want to use the `:css` property you have to use the `jsx` helper or create a component with `defstyled` 132 | 133 | ```clojure 134 | (em/defstyled div :div {}) 135 | 136 | ;; in your render function: 137 | [div {:css [{:background "blue"} {:color "#eff"}]} "Some text"] 138 | ``` 139 | 140 | A cool future development would be to plug into the Reagent compiler and add support for `:css` on all elements. 141 | 142 | Pull requests welcome if that interests you! 143 | 144 | More examples in the devcards: 145 | 146 | https://dvingo.github.io/cljs-emotion/#!/dv.cljs_emotion.reagent_cards 147 | 148 | # Development 149 | 150 | ```bash 151 | make dev 152 | ``` 153 | 154 | This starts the shadow-cljs server and watch process for the devcards build. 155 | 156 | Open the shadow-cljs devtools webpage and then click on the webserver that hosts the devcards. 157 | 158 | ## Deploy 159 | 160 | Notes for deploy: 161 | 162 | - update pom.xml version 163 | - commit 164 | 165 | Build a deployable jar of this library: 166 | 167 | clojure -X:jar 168 | 169 | Install it locally: 170 | 171 | clojure -M:install 172 | 173 | Deploy it to Clojars -- needs `CLOJARS_USERNAME` and `CLOJARS_PASSWORD` environment variables: 174 | 175 | clojure -M:deploy 176 | 177 | ## Publish github pages 178 | 179 | See `Makefile` 180 | 181 | ```bash 182 | git checkout gh-pages 183 | git merge master 184 | make gh-pages 185 | git add -A 186 | git commit -m 187 | git push github HEAD -f 188 | ``` 189 | -------------------------------------------------------------------------------- /src/main/dv/emotion_helix_dom.cljc: -------------------------------------------------------------------------------- 1 | ;; copied from https://github.com/lilactown/helix/blob/ddfa2de50804f84bb6e8100ed278cbe05cff128e/src/helix/dom.cljc 2 | (ns dv.emotion-helix-dom 3 | #?(:cljs (:require-macros [dv.emotion-helix-dom])) 4 | (:refer-clojure :exclude [map meta time]) 5 | (:require 6 | #?(:cljs ["@emotion/react" :as em.react]) 7 | [borkdude.dynaload :refer [dynaload]] 8 | [clojure.walk :as walk] 9 | [dv.emotion-css :as dv.em])) 10 | 11 | #?(:cljs (def jsx em.react/jsx)) 12 | #?(:cljs (def css dv.em/css)) 13 | 14 | (declare 15 | input textarea option select a abbr address area article aside audio b base bdi 16 | bdo big blockquote body br button canvas caption cite code col colgroup data datalist 17 | dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form 18 | h1 h2 h3 h4 h5 h6 head header hr html i iframe img ins kbd keygen label legend li link 19 | main map mark menu menuitem meta meter nav noscript object ol optgroup output p param 20 | picture pre progress q rp rt ruby s samp script section small source span strong style 21 | sub summary sup table tbody td tfoot th thead time title tr track u ul var video wbr 22 | circle clipPath ellipse g line mask path pattern polyline rect svg text defs 23 | linearGradient polygon radialGradient stop tspan) 24 | 25 | (def tags 26 | '[input textarea option select 27 | a 28 | abbr 29 | address 30 | area 31 | article 32 | aside 33 | audio 34 | b 35 | base 36 | bdi 37 | bdo 38 | big 39 | blockquote 40 | body 41 | br 42 | button 43 | canvas 44 | caption 45 | cite 46 | code 47 | col 48 | colgroup 49 | data 50 | datalist 51 | dd 52 | del 53 | details 54 | dfn 55 | dialog 56 | div 57 | dl 58 | dt 59 | em 60 | embed 61 | fieldset 62 | figcaption 63 | figure 64 | footer 65 | form 66 | h1 67 | h2 68 | h3 69 | h4 70 | h5 71 | h6 72 | head 73 | header 74 | hr 75 | html 76 | i 77 | iframe 78 | img 79 | ins 80 | kbd 81 | keygen 82 | label 83 | legend 84 | li 85 | link 86 | main 87 | map 88 | mark 89 | menu 90 | menuitem 91 | meta 92 | meter 93 | nav 94 | noscript 95 | object 96 | ol 97 | optgroup 98 | output 99 | p 100 | param 101 | picture 102 | pre 103 | progress 104 | q 105 | rp 106 | rt 107 | ruby 108 | s 109 | samp 110 | script 111 | section 112 | small 113 | source 114 | span 115 | strong 116 | style 117 | sub 118 | summary 119 | sup 120 | table 121 | tbody 122 | td 123 | tfoot 124 | th 125 | thead 126 | time 127 | title 128 | tr 129 | track 130 | u 131 | ul 132 | var 133 | video 134 | wbr 135 | 136 | ;; svg 137 | circle 138 | clipPath 139 | ellipse 140 | g 141 | line 142 | marker 143 | mask 144 | path 145 | pattern 146 | polyline 147 | rect 148 | svg 149 | text 150 | defs 151 | linearGradient 152 | polygon 153 | radialGradient 154 | stop 155 | tspan]) 156 | 157 | (def hx-merge-obj (dynaload 'helix.impl.props/merge-obj)) 158 | (def hx-primitive-obj (dynaload 'helix.impl.props/primitive-obj)) 159 | (def hx-set-obj (dynaload 'helix.impl.props/set-obj)) 160 | (def hx-kw->str (dynaload 'helix.impl.props/kw->str)) 161 | (def hx-camel-case (dynaload 'helix.impl.props/camel-case)) 162 | (def hx-or-undefined (dynaload 'helix.impl.props/or-undefined)) 163 | (def hx-normalize-class (dynaload 'helix.impl.props/normalize-class)) 164 | (def hx-dom-style (dynaload 'helix.impl.props/dom-style)) 165 | 166 | (defn -dom-props 167 | ([m] #?(:clj (if-let [spread-sym (cond 168 | (contains? m '&) '& 169 | (contains? m :&) :&)] 170 | `(@hx-merge-obj ~(-dom-props (dissoc m spread-sym) (@hx-primitive-obj)) 171 | (-dom-props ~(get m spread-sym))) 172 | (-dom-props m (@hx-primitive-obj))) 173 | :cljs (if (map? m) 174 | (-dom-props m (@hx-primitive-obj)) 175 | ;; assume JS obj 176 | m))) 177 | ([m o] 178 | (if (seq m) 179 | (recur (rest m) 180 | (let [entry (first m) 181 | k (key entry) 182 | v (val entry)] 183 | (case k 184 | :css (@hx-set-obj o "css" #?(:clj `(dv.em/convert-css ~v) :cljs (dv.em/convert-css v))) 185 | :class (@hx-set-obj o "className" (@hx-normalize-class v)) 186 | :for (@hx-set-obj o "htmlFor" v) 187 | :style (@hx-set-obj o "style" (@hx-dom-style v)) 188 | :value (@hx-set-obj o "value" #?(:clj `(@hx-or-undefined ~v) :cljs (@hx-or-undefined v))) 189 | (@hx-set-obj o (@hx-camel-case (@hx-kw->str k)) v)))) 190 | #?(:clj (list* o) 191 | :cljs o)))) 192 | 193 | (defmacro dom-props [m] (-dom-props m)) 194 | 195 | (defmacro $d 196 | "Creates a new React DOM element. \"type\" ought to be a string like \"span\", 197 | \"div\",etc. 198 | 199 | When a map of props are passed as the second argument, will statically convert 200 | to a JS object, specially handling things like converting kebab-case props to 201 | camelCase and other transformations. 202 | 203 | Use the special & or :& prop to merge dynamic props in." 204 | [type & args] 205 | (if (map? (first args)) 206 | `^js/React.Element (jsx ~type (dom-props ~(first args)) ~@(rest args)) 207 | `^js/React.Element (jsx ~type nil ~@args))) 208 | 209 | #?(:clj (defn gen-tag [tag] `(defmacro ~tag [& args#] `($d ~(str '~tag) ~@args#)))) 210 | #?(:clj (defmacro gen-tags [] `(do ~@(for [tag tags] (gen-tag tag))))) 211 | #?(:clj (gen-tags)) 212 | -------------------------------------------------------------------------------- /src/dev/dv/cljs_emotion/target_styled.cljs: -------------------------------------------------------------------------------- 1 | (ns dv.cljs-emotion.target-styled 2 | (:require 3 | [devcards.core :as dc :refer (defcard)] 4 | [sablono.core :refer [html]] 5 | ["polished" :as p :refer [darken lighten]] 6 | ["react" :as react] 7 | ["react-dom" :as react-dom] 8 | [dv.cljs-emotion :as em :refer [defstyled keyframes global-style]])) 9 | 10 | (defcard 11 | "These examples show some more emotion API examples.") 12 | 13 | (defstyled prop-fn :div {:padding 20 :outline "1px solid"}) 14 | 15 | (defstyled prop-fn2 :div 16 | {:padding 20 17 | :outline "1px solid" 18 | "& > a" {:color "hotpink"}}) 19 | 20 | (defcard 21 | "## Using :as 22 | You can change the DOM element at render time by passing `:as`. 23 | ```clojure 24 | (defstyled prop-fn :div {:padding 20 :outline \"1px solid\"}) 25 | (prop-fn {:as \"button\"} \"HERE\")\n 26 | ``` 27 | " 28 | (prop-fn {:as "button"} "HERE")) 29 | 30 | (defcard prop-fn 31 | " 32 | An & will be replaced by the current styled-component's class name - so you can 33 | use it how you wish - override styles by repeating it \"&&&\" for example, or in this case target 34 | a child element: 35 | ```clojure 36 | (defstyled prop-fn2 :div\n {:padding 20 \n :outline \"1px solid\"\n \"& > a\" {:color \"hotpink\"} } )\n 37 | 38 | (prop-fn2 (html [:a {:href \"localhost:9001\"} \"hi here\"]))\n 39 | ``` 40 | " 41 | (prop-fn2 (html [:a {:href "localhost:9001"} "hi here"])) 42 | ) 43 | 44 | (defstyled hover-example prop-fn 45 | {":hover,:focus" {:background "hotpink"}}) 46 | 47 | (defcard 48 | "You can combine multiple selectors using the CSS comma operator. 49 | 50 | ```clojure 51 | (defstyled hover-example prop-fn\n {\":hover,:focus\" {:background \"hotpink\"}})\n 52 | ```" 53 | (hover-example "HELLLO") 54 | ) 55 | 56 | (defstyled a-child :div 57 | {:color "deepSKYBlue"}) 58 | 59 | (defstyled a-parent :div 60 | {:color "red" 61 | a-child {:color "darkorchid"}}) 62 | 63 | (defn nested-child-ex [] 64 | (html 65 | [:div 66 | (a-child "child should be deepSkyBlue") 67 | (a-parent "parent should be red") 68 | (a-parent 69 | (a-child "nested child should be darkorchid"))])) 70 | 71 | (dc/defcard-doc 72 | "# Target another defstyled component 73 | If you use a component created with `defstyled` in the key position of a styles map 74 | the generated CSS uses a class selector in its place that is a hash of its fully qualified 75 | symbol name. 76 | 77 | This works inside media queries and functions (see the next example)." 78 | (dc/mkdn-pprint-source a-child) 79 | (dc/mkdn-pprint-source a-parent) 80 | (dc/mkdn-pprint-source nested-child-ex)) 81 | 82 | (defn use-current-width [] 83 | (let [use-width (react/useState js/innerWidth) 84 | width (aget use-width 0) 85 | set-width (aget use-width 1)] 86 | (react/useEffect 87 | (fn [] 88 | (let [on-resize 89 | (fn [timeout-id] 90 | (js/clearTimeout timeout-id) 91 | (js/setTimeout (fn [] (set-width js/innerWidth)) 150))] 92 | (js/addEventListener "resize" on-resize) 93 | (fn [] (js/removeEventListener "resize" on-resize))))) 94 | width)) 95 | 96 | (defstyled a-parent2 :div 97 | (fn [{:keys [color]}] 98 | {:color "red" 99 | a-child {:color (or color "darkorchid")} 100 | "@media (min-width: 1024px)" 101 | {a-child {:color "black"}}})) 102 | 103 | (defn my-component [] 104 | (let [width (use-current-width)] 105 | (html 106 | [:div 107 | (a-parent2 {:color "blue"} "parent should be red") 108 | (a-child "child should be deepSkyBlue") 109 | (a-parent2 {:color "steelblue"} 110 | (a-child 111 | (str "nested child should be " 112 | (if (>= width 1024) "black" "darkorchid"))))]))) 113 | 114 | (dc/defcard-doc 115 | "# Target another defstyled component in nested position 116 | Here we change the child element conditionally based on a media query - 117 | resize the page over and under 1024 pixels to see the effect." 118 | (dc/mkdn-pprint-source a-parent2) 119 | (dc/mkdn-pprint-source my-component)) 120 | 121 | (defcard 122 | (dc/dom-node 123 | (fn [data-atom node] 124 | (react-dom/render (react/createElement my-component) node)))) 125 | 126 | 127 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 128 | ;; styled component in selector string 129 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 130 | 131 | (defstyled a-parent3 :div 132 | (fn [_] 133 | {:color "red" 134 | (str a-child " + " a-child) 135 | {:color "#eee" 136 | :background-color "hsl(0, 0%, 48%)" 137 | :padding "1em" 138 | :border-top "1px solid"} 139 | a-child {:color "darkorchid" 140 | :background-color "paleVIOLETRed"} 141 | "@media (min-width: 1024px)" 142 | {a-child {:color "black"} 143 | (str a-child " + " a-child) 144 | {:background-color (lighten 0.2 "hsl(0, 0%, 48%)")}}})) 145 | 146 | (dc/defcard-doc 147 | "# Target another defstyled component in a combinator selector" 148 | (dc/mkdn-pprint-source a-parent3) 149 | "Here we are using a styled component as part of a larger CSS Selector expression. 150 | This works by implementing `toString` for styled components, returning their class selector." 151 | " 152 | ```clojure 153 | (a-parent3 154 | \"HELLLO\" 155 | (a-child \"first\") 156 | (a-child \"second\") 157 | (a-child \"third\") 158 | (a-child \"fourth\") 159 | (a-child \"fifth\")) 160 | ```") 161 | 162 | (defcard 163 | (a-parent3 164 | "HELLLO" 165 | (a-child "first") 166 | (a-child "second") 167 | (a-child "third") 168 | (a-child "fourth") 169 | (a-child "fifth"))) 170 | 171 | (defstyled inside-text :span 172 | {:color "hsl(200, 40, 69)"}) 173 | 174 | (defn anon-target [] 175 | (em/jsx :div 176 | {:css 177 | {:color "skyblue" 178 | inside-text {:color "grey"}} } 179 | (html 180 | [:div 181 | [:p "outside text"] 182 | (inside-text "inside text")]))) 183 | -------------------------------------------------------------------------------- /src/dev/dv/cljs_emotion/reagent_cards.cljs: -------------------------------------------------------------------------------- 1 | (ns dv.cljs-emotion.reagent-cards 2 | (:require 3 | ["polished" :as p :refer [darken lighten]] 4 | [devcards.core :as dc :refer (defcard defcard-rg)] 5 | [reagent.core :as r] 6 | [dv.cljs-emotion-reagent :refer [jsx css defstyled keyframes global-style theme-provider]])) 7 | 8 | (defcard 9 | "These examples demonstrate reagent use.") 10 | 11 | (defstyled basic :div 12 | {:background "palevioletred"}) 13 | 14 | (defcard basic-card 15 | " 16 | ```clojure 17 | (defstyled basic :div\n {:background \"palevioletred\"})\n 18 | [basic \"Simple usage example\"]) \n 19 | ``` 20 | " 21 | (dc/reagent [basic "Simple usage example"])) 22 | 23 | (defcard basic-card2 24 | " 25 | ```clojure 26 | (defstyled basic :div 27 | {:background \"palevioletred\"}) 28 | 29 | [basic [:h2 \"Child \"]]) 30 | 31 | ``` 32 | " 33 | (dc/reagent [basic [:h2 "Child "]])) 34 | 35 | (defcard basic-card3 36 | "With props 37 | 38 | ```clojure 39 | [basic {:onClick #(js/console.log \"CLICK\")} [:h2 \"Child \"]] 40 | ``` 41 | " 42 | (dc/reagent [basic {:onClick #(js/console.log "CLICK")} [:h2 "Child "]])) 43 | (defcard basic-card4 44 | "With props and multiple children 45 | 46 | ```clojure 47 | [basic {:onClick #(js/console.log \"CLICK\")}\n [:h2 \"Child \"]\n [:h3 \"another child\"]] 48 | ``` 49 | " 50 | (dc/reagent [basic {:onClick #(js/console.log "CLICK")} 51 | [:h2 "Child "] 52 | [:h3 "another child"]])) 53 | 54 | (defstyled test-theme :div 55 | (fn [{:keys [theme]}] 56 | {:background (or (:bg theme) "blue")})) 57 | 58 | (defcard theme-provider-card 59 | "Theme use is straight forward. 60 | 61 | It wraps emotion's ThemeProvider calling clj->js first on the props. 62 | Wrap your application with `theme-provider` passing it 63 | a map or JS object of theme data. 64 | 65 | ```clojure 66 | (defstyled test-theme :div\n (fn [{:keys [theme]}]\n {:background (or (:bg theme) \"blue\")})) 67 | 68 | [:div\n (theme-provider {:theme {:bg \"yellow\"}}\n [test-theme \"Hello there theme\"])\n [test-theme \"no theme\"]] 69 | ``` 70 | " 71 | (dc/reagent 72 | [:div 73 | (theme-provider {:theme {:bg "yellow"}} 74 | [test-theme "Hello there theme"]) 75 | [test-theme "no theme"]])) 76 | 77 | (defstyled test-fn :div 78 | (fn [{:keys [text-case background-color] :as props}] 79 | (js/console.log "in defstyled fn props: " props) 80 | {:text-transform text-case 81 | :background-color background-color})) 82 | 83 | (defcard test-props-card 84 | "Testing the camel case logic" 85 | (dc/reagent 86 | [test-fn {:text-case "uppercase" 87 | :background-color "chartreuse"} "hi"])) 88 | 89 | (defstyled a-child :div 90 | {:color "deepSKYBlue"}) 91 | 92 | (defstyled a-parent :div 93 | {:color "red" 94 | a-child {:color "darkorchid"}}) 95 | 96 | (defcard-rg a-thing 97 | "# Target another defstyled component 98 | If you use a `defstyled` in key position in the styles map a CSS classname is used in its place that is a hash of its fully qualified 99 | symbol name. 100 | 101 | This works inside media queries and functions (see the next example). 102 | 103 | ```clojure 104 | (defstyled a-child :div 105 | {:color \"deepSKYBlue\"}) 106 | 107 | (defstyled a-parent :div 108 | {:color \"red\" 109 | a-child {:color \"darkorchid\"}}) 110 | 111 | [:div 112 | [a-child \"child should be deepSkyBlue\"] 113 | [a-parent \"parent should be red\"] 114 | [a-parent 115 | [a-child \"nested child should be darkorchid\"]]] 116 | ``` 117 | " 118 | [:div 119 | [a-child "child should be deepSkyBlue"] 120 | [a-parent "parent should be red"] 121 | [a-parent 122 | [a-child "nested child should be darkorchid"]]]) 123 | 124 | (defstyled a-parent2 :div 125 | (fn [{:keys [color]}] 126 | {:color "red" 127 | a-child {:color (or color "darkorchid")} 128 | "@media (min-width: 1024px)" 129 | {a-child {:color "black"}}})) 130 | 131 | (defcard-rg a-thing2 132 | "# Target another defstyled component (continued) 133 | ```clojure 134 | (defstyled a-child :div 135 | {:color \"deepSKYBlue\"}) 136 | 137 | (defstyled a-parent2 :div 138 | (fn [{:keys [color]}] 139 | {:color \"red\" 140 | a-child {:color (or color \"darkorchid\")} 141 | \"@media (min-width: 1024px)\" 142 | {a-child {:color \"black\"}}})) 143 | 144 | [:div 145 | [a-child \"child should be deepSkyBlue\"] 146 | [a-parent \"parent should be red\"] 147 | [a-parent 148 | [a-child \"nested child should be darkorchid, or black if window is >= 1024px\"]]] 149 | ``` 150 | " 151 | [:div 152 | [a-child "child should be deepSkyBlue"] 153 | [a-parent2 "parent should be red"] 154 | [a-parent2 {:color "steelblue"} 155 | [a-child "nested child should be darkorchid"]]]) 156 | 157 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 158 | ;; styled component in selector string 159 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 160 | 161 | (defstyled a-parent3 :div 162 | (fn [_] 163 | {:color "red" 164 | (str a-child " + " a-child) 165 | {:color "#eee" 166 | :background-color "hsl(0, 0%, 48%)" 167 | :padding "1em" 168 | :border-top "1px solid"} 169 | a-child {:color "darkorchid" 170 | :background-color "paleVIOLETRed"} 171 | "@media (min-width: 1024px)" 172 | {a-child {:color "black"} 173 | (str a-child " + " a-child) 174 | {:background-color (lighten 0.2 "hsl(0, 0%, 48%)")}}})) 175 | 176 | (dc/defcard-doc 177 | "# Target another defstyled component in a combinator selector" 178 | (dc/mkdn-pprint-source a-parent3) 179 | "Here we are using a styled component as part of a larger CSS Selector expression. 180 | This works by implementing `toString` for styled components, returning their class selector." 181 | " 182 | ```clojure 183 | [:div 184 | [a-parent3 185 | \"HELLLO\" 186 | [a-child \"first\"] 187 | [a-child \"second\"] 188 | [a-child \"third\"] 189 | [a-child \"fourth\"] 190 | [a-child \"fifth\"]]] 191 | ```") 192 | 193 | (defcard 194 | (dc/reagent 195 | [:div 196 | [a-parent3 197 | "HELLLO" 198 | [a-child "first"] 199 | [a-child "second"] 200 | [a-child "third"] 201 | [a-child "fourth"] 202 | [a-child "fifth"]]])) 203 | 204 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 205 | ;; Animation 206 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 207 | 208 | (defstyled with-anim :div 209 | (fn animation-styles [{:keys [time]}] 210 | {:animation (str 211 | (keyframes {:from {:background-color "transparent"} 212 | :to {:background-color "grey"}}) 213 | " " time "s ease-in-out infinite")})) 214 | 215 | (defn animation-card [a _] 216 | [:div 217 | [:p "animation time: " (:time @a) " seconds"] 218 | [:button {:on-click #(swap! a update :time inc)} "inc"] 219 | [:button {:on-click #(swap! a update :time dec)} "dec"] 220 | (with-anim {:time (:time @a)} "Some text here")]) 221 | 222 | (dc/defcard-doc 223 | "Keyframe animations are supported - this is built into emotion. " 224 | (dc/mkdn-pprint-source with-anim) 225 | (dc/mkdn-pprint-source animation-card)) 226 | 227 | (def animation-state (r/atom {:time 1})) 228 | 229 | (defcard-rg keyframes 230 | [animation-card animation-state] 231 | animation-state) 232 | 233 | (defn anon-styles [] 234 | (r/as-element 235 | [:div 236 | (jsx :div {:css {:background "lightgrey"}} 237 | [:p "Some text on a lightgrey background."]) 238 | (theme-provider {:theme {:bg "salmon"}} 239 | (jsx :div {:css 240 | [{:color "white"} 241 | #js{:border "1px solid"} 242 | (fn [t] 243 | (.log js/console "THEME : " t) 244 | {:background (or (:bg t) "lightgrey")})]} 245 | [:p "Some text on a salmon background."]))])) 246 | 247 | (dc/defcard-doc 248 | "# Anonymous inline styles support. 249 | You can use the `jsx` helper to style a react element inline without needing to create a component." 250 | (dc/mkdn-pprint-source anon-styles)) 251 | 252 | (defcard (anon-styles)) 253 | 254 | 255 | ;; Test for nested breakpoints in callback functions. 256 | ;=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 257 | 258 | (defstyled full-height :div 259 | {:height "100%" 260 | :background "pink"} 261 | (fn [props] 262 | (.log js/console "PROPS: " props) 263 | (:styles props))) 264 | 265 | (defn nested-fn [] 266 | [:div 267 | [full-height 268 | {:styles {:width "70%" "@media (min-width: 700px)" {:background "skyBLUE" :width "100%"}}} 269 | [:p "hello body"]]]) 270 | 271 | (defcard "Cljs properties should be passed to callbacks untouched." (dc/mkdn-pprint-source full-height)) 272 | 273 | ;(defcard breakpoint-debug (dc/mkdn-pprint-source nested-fn)) 274 | 275 | ;(defcard-rg (nested-fn)) 276 | 277 | ;(defcard-rg 278 | ; "Testing 1-arity test." 279 | ; [:div 280 | ; [full-height 281 | ; {:styles {":before" 282 | ; {:content "'HELLO'" 283 | ; "@media (min-width: 700px)" {:content "'LARGE SCREEN'"}} 284 | ; :width "70%" "@media (min-width: 700px)" {:background "skyBLUE" :width "100%"}}}]]) 285 | -------------------------------------------------------------------------------- /src/main/dv/emotion_css.cljc: -------------------------------------------------------------------------------- 1 | (ns dv.emotion-css 2 | (:require 3 | #?@(:cljs [["react" :as react] 4 | ["@emotion/hash" :as emotion-hash*] 5 | ["@emotion/react" :as styled-core :refer [Global ThemeProvider]] 6 | [goog.object :as g]]) 7 | [clojure.string :as str] 8 | [clojure.walk :as walk]) 9 | #?(:cljs (:require-macros [dv.cljs-emotion :refer [defstyled]]))) 10 | 11 | ;; Support plain cljs compiler and shadow. 12 | #?(:cljs (def emotion-hash (g/get emotion-hash* "default"))) 13 | #?(:cljs (def jsx* styled-core/jsx)) 14 | 15 | ;; Used to prevent generated code from needing to require goog.object 16 | (defn obj-set [o k v] 17 | #?(:cljs (g/set o k v) 18 | :clj nil)) 19 | 20 | (defn obj-get [o k] 21 | #?(:cljs (g/get o k) 22 | :clj nil)) 23 | 24 | ;; from fulcro 25 | #?(:cljs 26 | (defn force-children 27 | "Utility function that will force a lazy sequence of children (recursively) into realized 28 | vectors (React cannot deal with lazy seqs in production mode)" 29 | [x] 30 | (if (seq? x) 31 | (to-array (mapv force-children x)) 32 | x))) 33 | 34 | #?(:cljs 35 | (defn kebab->camel 36 | [prop] 37 | (if (str/starts-with? prop ".") 38 | prop 39 | 40 | (if (str/includes? prop "-") 41 | (let [words (->> (re-seq #"[a-zA-Z]+" prop) 42 | (mapv str/capitalize))] 43 | (-> words 44 | (update 0 str/lower-case) 45 | str/join)) 46 | prop)))) 47 | 48 | ;; todo use the caching strategy seen in reagent.impl.template 49 | 50 | #?(:cljs 51 | (defn camelize-keys 52 | "Also replaces styled components with their css classname is key position." 53 | [style-map] 54 | (walk/postwalk 55 | (fn in-walk [v] 56 | (cond 57 | (keyword? v) 58 | (-> v name kebab->camel) 59 | 60 | (and (meta v) (contains? (meta v) ::hashed-name)) 61 | (str "." (-> v meta ::hashed-name)) 62 | 63 | :else v)) 64 | style-map))) 65 | 66 | #?(:cljs 67 | (defn keyframes [anim-map] 68 | (styled-core/keyframes (clj->js (camelize-keys anim-map))))) 69 | 70 | (def cljs-props-key "dv.cljs-emotion/props") 71 | 72 | #?(:cljs 73 | (defn wrap-call-style-fn [anon-styles?] 74 | (fn [x] 75 | (cond 76 | 77 | ;; Another emotion styled component created with this lib. 78 | (and (meta x) (contains? (meta x) ::hashed-name)) 79 | (str "." (-> x meta ::hashed-name)) 80 | 81 | (fn? x) 82 | (fn [arg] 83 | ;; arg# is js props passed at runtime, we ship it back and forth js -> cljs -> js 84 | 85 | ;; js->clj is resulting in an infinite recur when children contains another styled component, so we remove it. 86 | (js-delete arg "children") 87 | 88 | (if anon-styles? 89 | ;; with anonymous styles there can be no props - so the theme is passed as the only argument 90 | (clj->js (camelize-keys (x (js->clj arg :keywordize-keys true)))) 91 | (let [cljs-args (assoc (obj-get arg cljs-props-key) 92 | :theme (js->clj (obj-get arg "theme") :keywordize-keys true))] 93 | ;; invoke the user-supplied function which returns style data - convert whatever they return to js data structures. 94 | (clj->js (camelize-keys (x cljs-args)))))) 95 | 96 | ;; maps come up in value position for nested selectors 97 | (map? x) 98 | (camelize-keys x) 99 | 100 | :else x)))) 101 | 102 | #?(:cljs (goog-define ADD_CLASSNAMES "INITIAL")) 103 | 104 | #?(:cljs 105 | (defn add-class-names? [] 106 | (if (boolean? ADD_CLASSNAMES) 107 | ADD_CLASSNAMES 108 | goog.DEBUG))) 109 | 110 | #?(:cljs 111 | (defn add-class-name [props class-name] 112 | (if (object? props) 113 | (doto props 114 | (goog.object/set "className" 115 | (->> [class-name (goog.object/get props "className")] 116 | (str/join " ") 117 | (str/trim)))) 118 | (update props :className #(if (nil? %) class-name (str class-name " " %)))))) 119 | 120 | #?(:cljs (defn hashit [string] (str "dvcss-" (emotion-hash string)))) 121 | 122 | #?(:cljs 123 | (defn set-class-name [props class-name] 124 | (if class-name 125 | (let [hashed-name (hashit class-name) 126 | props (add-class-name props hashed-name)] 127 | (if (add-class-names?) 128 | (add-class-name props class-name) 129 | props)) 130 | props))) 131 | 132 | #?(:cljs 133 | (defn map->obj [m] 134 | (reduce-kv (fn [o k v] 135 | ;; convert keywords to string only in key position 136 | (let [new-k (cond-> k (implements? INamed k) name) 137 | new-v (cond-> v (map? v) map->obj)] 138 | (doto o (obj-set new-k new-v)))) 139 | #js{} m))) 140 | 141 | #?(:cljs 142 | (defn make-js-props [props class-name] 143 | (if (object? props) 144 | props 145 | (let [clj-props (set-class-name props class-name) 146 | js-props (map->obj clj-props)] 147 | (doto js-props (obj-set cljs-props-key clj-props)))))) 148 | 149 | #?(:cljs 150 | (defn react-factory [el class-name] 151 | (fn 152 | ([] 153 | (jsx* el (clj->js (set-class-name {} class-name)))) 154 | ([props] 155 | (try 156 | (cond 157 | (or (react/isValidElement props) (string? props)) 158 | (jsx* el (set-class-name #js{} class-name) props) 159 | 160 | (map? props) 161 | ;; Do not use clj->js in order to preserve clojure data types like keywords that would not 162 | ;; survive a round-trip clj->js js->clj 163 | (jsx* el (make-js-props props class-name)) 164 | 165 | (object? props) 166 | (jsx* el (set-class-name props class-name)) 167 | 168 | (or (array? props) (coll? props)) 169 | (jsx* el (set-class-name #js{} class-name) (force-children props)) 170 | 171 | :else 172 | (jsx* el (set-class-name #js{} class-name))) 173 | 174 | (catch js/Object e 175 | (js/console.error "Error invoking an emotion styled component: " e)))) 176 | 177 | ([props & children] 178 | ;; if props are a mapping type and not a react child 179 | (if (or (and (object? props) (not (react/isValidElement props))) (map? props)) 180 | (let [js-props (make-js-props props class-name)] 181 | (if (seq children) 182 | (apply jsx* el js-props (force-children children)) 183 | (jsx* el js-props))) 184 | (apply jsx* el (set-class-name #js{} class-name) (force-children (list* props children)))))))) 185 | 186 | #?(:clj 187 | (defn get-type 188 | [styled-arg tag-name] 189 | (cond 190 | ;; if literals, don't need to determine type at runtime 191 | ;; a dom element like :div, same as styled.div`` 192 | (string? tag-name) `(obj-get ~styled-arg ~tag-name) 193 | (keyword? tag-name) `(obj-get ~styled-arg ~(name tag-name)) 194 | :else 195 | `(cond 196 | (string? ~tag-name) 197 | (obj-get ~styled-arg ~tag-name) 198 | 199 | (keyword? ~tag-name) 200 | (obj-get ~styled-arg ~(name tag-name)) 201 | 202 | ;; Another styled component 203 | (::styled (meta ~tag-name)) 204 | (.call ~styled-arg ~styled-arg (::styled (meta ~tag-name))) 205 | 206 | ;; A React component 207 | :else 208 | (.call ~styled-arg ~styled-arg ~tag-name))))) 209 | 210 | #?(:clj 211 | (defn get-cls-name 212 | [namespace-name print-style component-sym] 213 | (case print-style 214 | :full (str namespace-name "/" component-sym) 215 | :short (str component-sym) 216 | :nil nil))) 217 | 218 | #?(:clj (def default-classname-style :full)) 219 | 220 | #?(:clj 221 | (defn get-cls-name-from-meta 222 | "Returns string or nil for the classname" 223 | [namespace-name component-sym] 224 | (if (contains? (meta component-sym) :styled/classname) 225 | (let [print-config (:styled/classname (meta component-sym))] 226 | (if (#{:full :short :nil} print-config) 227 | (get-cls-name namespace-name print-config component-sym) 228 | (throw (Exception. (str "Unknown option for class-name style in metadata passed to component: " component-sym))))) 229 | (get-cls-name namespace-name default-classname-style component-sym)))) 230 | 231 | ;; emotion doesn't allow functions in nested position, only 232 | ;; objects and arrays of objects 233 | ;; but they do allow one function as a child 234 | ;; you can always wrap the call in a fn if you want dynamism like below 235 | ;https://github.com/emotion-js/emotion/blob/188dc0e785cfc9b10b3f9a6ead62b56ddd38e039/packages/core/src/global.js#L16 236 | 237 | #?(:cljs 238 | (defn global-style 239 | "Takes a cljs vector or hashmap of styles and converts to JS types before calling emotion's Global function 240 | from the @emotion/react package. 241 | Inserts styles into the DOM that target global elements and classes. 242 | 243 | https://emotion.sh/docs/@emotion/react" 244 | [props] 245 | (react/createElement Global #js{:styles (clj->js (camelize-keys props))}))) 246 | 247 | ;; can use like so: 248 | (comment 249 | (global-style {:body {:background "#cce" "@media (min-width:700px)" {:background "white"}}}) 250 | (global-style 251 | [(clj->js {:body {:font-family "serif"}}) {:body {:border "2px solid yellOW"}} {:body {:background-color "#ecccee"}}]) 252 | 253 | ;; to adapt based on props, wrap in a fn: 254 | (defn my-globals [props] 255 | (global-style 256 | {:body {:background-color "red"}}))) 257 | 258 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 259 | ;; Theme support 260 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 261 | 262 | #?(:cljs 263 | (defn theme-provider 264 | "Takes a hashmap of a style theme and react children to render with that theme in the React Context 265 | using emotion's ThemeProvider." 266 | [props & children] 267 | (when-not (contains? props :theme) (throw (js/Error. "You must pass a :theme to the theme-provider."))) 268 | (if (:theme props) 269 | (apply react/createElement ThemeProvider 270 | (clj->js props) 271 | (force-children children)) 272 | (react/Children.map children (fn [c i] (react/cloneElement c #js{:key i})))))) 273 | 274 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 275 | ;; CSS prop support 276 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 277 | 278 | #?(:clj 279 | (defn ^:private css-body [props] 280 | `(do 281 | ;(assert (contains? ~props :css) "Props must contain :css key") 282 | (cljs.core/clj->js 283 | (assoc ~props :css 284 | (walk/postwalk 285 | ;; todo here you can do props validation also 286 | ;; should not allow anything that's not a symbol, map, vector, js-obj, js-array, fn 287 | (wrap-call-style-fn true) 288 | (:css ~props))))))) 289 | #?(:clj 290 | (defmacro jsx 291 | ([el props] 292 | (let [el (cond-> el (keyword? el) name) 293 | css-props (css-body props)] 294 | `(jsx* ~el ~css-props))) 295 | 296 | ([el props & children] 297 | (let [el (cond-> el (keyword? el) name) 298 | css-props (css-body props)] 299 | `(jsx* ~el ~css-props ~@children))))) 300 | 301 | #?(:cljs 302 | (defn convert-css 303 | "Takes a hashmap or vector of styles and converts to JS types, will pass any functions cljs data structures." 304 | [css] 305 | (clj->js (walk/postwalk (wrap-call-style-fn true) css)))) 306 | 307 | #?(:cljs 308 | (defn css [css] 309 | (styled-core/css 310 | (if (object? css) 311 | css 312 | (clj->js (walk/postwalk (wrap-call-style-fn true) css)))))) 313 | -------------------------------------------------------------------------------- /src/main/dv/cljs_emotion_reagent.cljc: -------------------------------------------------------------------------------- 1 | (ns dv.cljs-emotion-reagent 2 | (:require 3 | #?@(:cljs [["react" :as react] 4 | ["@emotion/hash" :as emotion-hash*] 5 | ["@emotion/styled" :as styled*] 6 | ["@emotion/react" :as styled-core :refer [Global ThemeProvider]]]) 7 | #?@(:cljs 8 | [[borkdude.dynaload :refer [dynaload]] 9 | [goog.object :as g]]) 10 | [camel-snake-kebab.core :as csk] 11 | [clojure.walk :as walk] 12 | [clojure.string :as str]) 13 | #?(:cljs (:require-macros [dv.cljs-emotion-reagent :refer [defstyled]]))) 14 | 15 | ;; This is so consumers of this library don't need to pull in reagent. 16 | #?(:cljs (def r-as-element (dynaload 'reagent.core/as-element))) 17 | #?(:cljs (def r-convert-prop-value (dynaload 'reagent.impl.template/convert-prop-value))) 18 | #?(:cljs (def r-class-names (dynaload 'reagent.impl.util/class-names))) 19 | ;; Support plain cljs compiler and shadow. 20 | #?(:cljs (def emotion-hash (g/get emotion-hash* "default"))) 21 | #?(:cljs (def styled (g/get styled* "default"))) 22 | #?(:cljs (def jsx* styled-core/jsx)) 23 | 24 | ;; Used to prevent generated code from needing to require goog.object 25 | (defn obj-set [o k v] 26 | #?(:cljs (g/set o k v) 27 | :clj nil)) 28 | 29 | (defn obj-get [o k] 30 | #?(:cljs (g/get o k) 31 | :clj nil)) 32 | 33 | #?(:cljs 34 | (defn relement? 35 | "Is it a reagent vector? (or a best effort guess at least.)" 36 | [el] 37 | (and 38 | (vector? el) 39 | (let [item (first el)] 40 | (or 41 | (keyword? item) 42 | (symbol? item) 43 | (fn? item)))))) 44 | 45 | ;; from fulcro 46 | #?(:cljs 47 | (defn force-children 48 | "Utility function that will force a lazy sequence of children (recursively) into realized 49 | vectors (React cannot deal with lazy seqs in production mode)" 50 | [x] 51 | (if (seq? x) 52 | (to-array (mapv force-children x)) 53 | (if (relement? x) 54 | (@r-as-element x) 55 | x)))) 56 | 57 | #?(:cljs 58 | (defn kebab->camel [prop] 59 | (if (str/starts-with? prop ".") 60 | prop 61 | 62 | (if (str/includes? prop "-") 63 | (let [words (->> (re-seq #"[a-zA-Z]+" prop) 64 | (mapv str/capitalize))] 65 | (-> words 66 | (update 0 str/lower-case) 67 | str/join)) 68 | prop)))) 69 | 70 | #?(:cljs 71 | (defn camelize-keys 72 | "Takes a clj map and returns that map modified. 73 | Postwalk replaces kebab keywords with camel case version as is expected by React. 74 | 75 | Also replaces styled components with their CSS classname when they appear in key position." 76 | [style-map] 77 | (walk/postwalk 78 | (fn [v] 79 | (cond 80 | (keyword? v) 81 | (-> v name kebab->camel) 82 | 83 | (and (meta v) (contains? (meta v) ::hashed-name)) 84 | (str "." (-> v meta ::hashed-name)) 85 | 86 | :else v)) 87 | style-map))) 88 | 89 | #?(:cljs 90 | (defn keyframes [anim-map] 91 | (styled-core/keyframes (clj->js (camelize-keys anim-map))))) 92 | 93 | (def cljs-props-key "dv.cljs-emotion/props") 94 | 95 | #?(:cljs 96 | (defn wrap-call-style-fn [anon-styles?] 97 | (fn [x] 98 | (cond 99 | 100 | ;; Another emotion styled component created with this lib. 101 | (and (meta x) (contains? (meta x) ::hashed-name)) 102 | (str "." (-> x meta ::hashed-name)) 103 | 104 | (fn? x) 105 | (fn [arg] 106 | ;; arg# is js props passed at runtime, we ship it back and forth js -> cljs -> js 107 | 108 | ;; js->clj is resulting in an infinite recur when children contains another styled component, so we remove it. 109 | (js-delete arg "children") 110 | 111 | (if anon-styles? 112 | ;; with anonymous styles there can be no props - so the theme is passed as the only argument 113 | (clj->js (camelize-keys (x (js->clj arg :keywordize-keys true)))) 114 | (let [cljs-args (assoc (obj-get arg cljs-props-key) 115 | :theme (js->clj (obj-get arg "theme") :keywordize-keys true))] 116 | ;; invoke the user-supplied function which returns style data - convert whatever they return to js data structures. 117 | (clj->js (camelize-keys (x cljs-args)))))) 118 | 119 | ;; maps come up in value position for nested selectors 120 | (map? x) 121 | (camelize-keys x) 122 | 123 | :else x)))) 124 | 125 | #?(:cljs (goog-define ADD_CLASSNAMES "INITIAL")) 126 | 127 | #?(:cljs 128 | (defn add-class-names? [] 129 | (if (boolean? ADD_CLASSNAMES) 130 | ADD_CLASSNAMES 131 | goog.DEBUG))) 132 | 133 | #?(:cljs 134 | (defn add-class-name [props class-name] 135 | (if (object? props) 136 | (doto props 137 | (obj-set "className" 138 | (->> [class-name (obj-get props "className")] 139 | (str/join " ") 140 | (str/trim)))) 141 | (update props :className #(if (nil? %) class-name (str class-name " " %)))))) 142 | 143 | #?(:cljs (defn hashit [string] (str "dvcss-" (emotion-hash string)))) 144 | 145 | #?(:cljs 146 | (defn set-class-name [props class-name] 147 | (if class-name 148 | (let [hashed-name (hashit class-name) 149 | props (add-class-name props hashed-name)] 150 | (if (add-class-names?) 151 | (add-class-name props class-name) 152 | props)) 153 | props))) 154 | 155 | #?(:cljs 156 | (defn map->obj [m] 157 | (reduce-kv (fn [o k v] 158 | ;; convert keywords to string only in key position 159 | (let [new-k (cond-> k (implements? INamed k) name) 160 | new-v (cond-> v (map? v) map->obj)] 161 | (doto o (obj-set new-k new-v)))) 162 | #js{} m))) 163 | 164 | #?(:cljs 165 | (defn make-js-props 166 | "Allows using kebab-case prop names." 167 | [props class-name] 168 | (if (object? props) 169 | props 170 | (let [clss (:class props) 171 | props (cond-> props clss (assoc :class (@r-class-names clss))) 172 | ;; converts properties for JS call as expected by react class->className, on-click->onClick etc. 173 | clj-props (set-class-name props class-name) 174 | js-props (@r-convert-prop-value props) 175 | js-props (set-class-name js-props class-name)] 176 | (doto js-props (obj-set cljs-props-key clj-props)))))) 177 | 178 | #?(:cljs 179 | (defn react-factory 180 | [el class-name] 181 | (fn 182 | ([] 183 | (jsx* el (clj->js (set-class-name {} class-name)))) 184 | ([props] 185 | (try 186 | (cond 187 | (or (react/isValidElement props) (string? props)) 188 | (jsx* el (set-class-name #js{} class-name) props) 189 | 190 | (map? props) 191 | (let [props (make-js-props props class-name)] 192 | (jsx* el props)) 193 | 194 | (object? props) 195 | (jsx* el (set-class-name props class-name)) 196 | 197 | (relement? props) 198 | (jsx* el (set-class-name #js{} class-name) (@r-as-element props)) 199 | 200 | (seq? props) 201 | (jsx* el (set-class-name #js{} class-name) (force-children props)) 202 | 203 | (array? props) 204 | (jsx* el (set-class-name #js{} class-name) props) 205 | 206 | :else 207 | (jsx* el (set-class-name #js{} class-name))) 208 | 209 | (catch js/Object e 210 | (js/console.error "Error invoking an emotion styled component: " e)))) 211 | 212 | ([props & children] 213 | ;; if props are a mapping type and not a react child 214 | (if (or (and (object? props) (not (react/isValidElement props))) (map? props)) 215 | (let [js-props (make-js-props props class-name)] 216 | (if (seq children) 217 | (apply jsx* el js-props (force-children children)) 218 | (jsx* el js-props))) 219 | (apply jsx* el (set-class-name #js{} class-name) (force-children (list* props children)))))))) 220 | 221 | #?(:clj 222 | (defn get-type 223 | [styled-arg tag-name] 224 | (cond 225 | ;; if literals, don't need to determine type at runtime 226 | (string? tag-name) `(obj-get ~styled-arg ~tag-name) 227 | (keyword? tag-name) `(obj-get ~styled-arg ~(name tag-name)) 228 | :else 229 | `(cond 230 | ;; a dom element like :div, same as styled.div`` 231 | (string? ~tag-name) 232 | (obj-get ~styled-arg ~tag-name) 233 | 234 | (keyword? ~tag-name) 235 | (obj-get ~styled-arg ~(name tag-name)) 236 | 237 | ;; Another styled component 238 | (::styled (meta ~tag-name)) 239 | (.call ~styled-arg ~styled-arg (::styled (meta ~tag-name))) 240 | 241 | ;; A React component 242 | :else 243 | (.call ~styled-arg ~styled-arg ~tag-name))))) 244 | 245 | #?(:clj 246 | (defn get-cls-name 247 | [namespace-name print-style component-sym] 248 | (case print-style 249 | :full (str namespace-name "/" component-sym) 250 | :short (str component-sym) 251 | :nil nil))) 252 | 253 | #?(:clj (def default-classname-style :full)) 254 | 255 | #?(:clj 256 | (defn get-cls-name-from-meta 257 | "Returns string or nil for the classname" 258 | [namespace-name component-sym] 259 | (if (contains? (meta component-sym) :styled/classname) 260 | (let [print-config (:styled/classname (meta component-sym))] 261 | (if (#{:full :short :nil} print-config) 262 | (get-cls-name namespace-name print-config component-sym) 263 | (throw (Exception. (str "Unknown option for class-name style in metadata passed to component: " component-sym))))) 264 | (get-cls-name namespace-name default-classname-style component-sym)))) 265 | 266 | #?(:clj 267 | (defmacro defstyled 268 | ([component-name el & children] 269 | (let [component-type (gensym "component-type") 270 | clss (gensym "clss") 271 | class-name (gensym "className") 272 | full-class-name (gensym "fullClassName") 273 | children* (gensym "children")] 274 | `(let [~class-name ~(get-cls-name-from-meta (-> &env :ns :name) component-name) 275 | ~full-class-name ~(str (-> &env :ns :name) "/" component-name) 276 | ~children* 277 | (walk/postwalk 278 | ;; todo here you can do props validation also 279 | ;; should not allow anything that's not a symbol, map, vector, js-obj, js-array, fn 280 | (wrap-call-style-fn false) 281 | ~(vec children)) 282 | ;; pass js structures to the lib 283 | ~children* (cljs.core/clj->js ~children*) 284 | ~component-type ~(get-type `styled el) 285 | ~clss (.apply ~component-type ~component-type ~children*)] 286 | (obj-set ~clss "displayName" ~(str (-> &env :ns :name) "/" component-name)) 287 | ;(goog.object/set ~clss "displayName" ~(str (-> &env :ns :name) "/" component-name)) 288 | 289 | (def ~component-name 290 | (with-meta (react-factory ~clss ~class-name) 291 | {::styled ~clss 292 | ::hashed-name (hashit ~full-class-name)})) 293 | 294 | (cljs.core/specify! ~component-name 295 | ;~'IPrintWithWriter 296 | ;(~'-pr-writer [this# writer# _#] 297 | ; (~'-write writer# (cljs.core/str this#))) 298 | ~'Object 299 | (~'toString [this#] 300 | (cljs.core/str "." (::hashed-name (meta ~component-name)))))))))) 301 | 302 | #?(:cljs 303 | (def ^:private global* (react-factory Global nil))) 304 | 305 | ;; emotion doesn't allow functions in nested position, only 306 | ;; objects and arrays of objects 307 | ;; but they do allow one function as a child 308 | ;; you can always wrap the call in a fn if you want dynamism like below 309 | ;https://github.com/emotion-js/emotion/blob/188dc0e785cfc9b10b3f9a6ead62b56ddd38e039/packages/core/src/global.js#L16 310 | 311 | #?(:cljs 312 | (defn global-style [props] 313 | (global* {:styles (camelize-keys props)}))) 314 | 315 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 316 | ;; Theme support 317 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 318 | 319 | #?(:cljs 320 | (defn theme-provider 321 | [props & children] 322 | (when-not (contains? props :theme) 323 | (throw (js/Error. "You must pass a :theme to the theme-provider."))) 324 | (apply react/createElement ThemeProvider 325 | (clj->js props) 326 | (force-children children)))) 327 | 328 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 329 | ;; CSS prop support 330 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 331 | 332 | #?(:clj 333 | (defn css-body [props] 334 | `(do 335 | (assert (contains? ~props :css) "Props must contain :css key") 336 | (cljs.core/clj->js 337 | (assoc ~props :css 338 | (walk/postwalk 339 | ;; todo here you can do props validation also 340 | ;; should not allow anything that's not a symbol, map, vector, js-obj, js-array, fn 341 | (wrap-call-style-fn true) 342 | (:css ~props))))))) 343 | #?(:clj 344 | (defmacro jsx 345 | ([el props] 346 | (let [el (cond-> el (keyword? el) name) 347 | css-props (css-body props)] 348 | `(jsx* ~el ~css-props))) 349 | 350 | ([el props children] 351 | (let [el (cond-> el (keyword? el) name) 352 | css-props (css-body props)] 353 | `(jsx* ~el ~css-props (reagent.core/as-element ~children)))))) 354 | 355 | #?(:cljs 356 | (defn css [& css] 357 | (styled-core/css (clj->js (walk/postwalk (wrap-call-style-fn true) css))))) 358 | -------------------------------------------------------------------------------- /src/main/dv/cljs_emotion.cljc: -------------------------------------------------------------------------------- 1 | (ns dv.cljs-emotion 2 | (:require 3 | #?@(:cljs [["react" :as react] 4 | ["@emotion/hash" :as emotion-hash*] 5 | ["@emotion/styled" :as styled*] 6 | ["@emotion/react" :as styled-core :refer [Global ThemeProvider]] 7 | [goog.object :as g]]) 8 | [clojure.string :as str] 9 | [clojure.walk :as walk]) 10 | #?(:cljs (:require-macros [dv.cljs-emotion :refer [defstyled]]))) 11 | 12 | ;; Support plain cljs compiler and shadow. 13 | #?(:cljs (def emotion-hash (g/get emotion-hash* "default"))) 14 | #?(:cljs (def styled (g/get styled* "default"))) 15 | #?(:cljs (def jsx* styled-core/jsx)) 16 | 17 | ;; Used to prevent generated code from needing to require goog.object 18 | (defn obj-set [o k v] 19 | #?(:cljs (g/set o k v) 20 | :clj nil)) 21 | 22 | (defn obj-get [o k] 23 | #?(:cljs (g/get o k) 24 | :clj nil)) 25 | 26 | ;; from fulcro 27 | #?(:cljs 28 | (defn force-children 29 | "Utility function that will force a lazy sequence of children (recursively) into realized 30 | vectors (React cannot deal with lazy seqs in production mode)" 31 | [x] 32 | (if (seq? x) 33 | (to-array (mapv force-children x)) 34 | x))) 35 | 36 | #?(:cljs 37 | (defn kebab->camel 38 | [prop] 39 | (if (str/starts-with? prop ".") 40 | prop 41 | 42 | (if (str/includes? prop "-") 43 | (let [words (->> (re-seq #"[a-zA-Z]+" prop) 44 | (mapv str/capitalize))] 45 | (-> words 46 | (update 0 str/lower-case) 47 | str/join)) 48 | prop)))) 49 | 50 | ;; todo use the caching strategy seen in reagent.impl.template 51 | 52 | #?(:cljs 53 | (defn camelize-keys 54 | "Also replaces styled components with their css classname is key position." 55 | [style-map] 56 | (walk/postwalk 57 | (fn in-walk [v] 58 | (cond 59 | (keyword? v) 60 | (-> v name kebab->camel) 61 | 62 | (and (meta v) (contains? (meta v) ::hashed-name)) 63 | (str "." (-> v meta ::hashed-name)) 64 | 65 | :else v)) 66 | style-map))) 67 | 68 | #?(:cljs 69 | (defn keyframes [anim-map] 70 | (styled-core/keyframes (clj->js (camelize-keys anim-map))))) 71 | 72 | (def cljs-props-key "dv.cljs-emotion/props") 73 | 74 | #?(:cljs 75 | (defn wrap-call-style-fn [anon-styles?] 76 | (fn [x] 77 | (cond 78 | 79 | ;; Another emotion styled component created with this lib. 80 | (and (meta x) (contains? (meta x) ::hashed-name)) 81 | (str "." (-> x meta ::hashed-name)) 82 | 83 | (fn? x) 84 | (fn [arg] 85 | ;; arg# is js props passed at runtime, we ship it back and forth js -> cljs -> js 86 | 87 | ;; js->clj is resulting in an infinite recur when children contains another styled component, so we remove it. 88 | (js-delete arg "children") 89 | 90 | (if anon-styles? 91 | ;; with anonymous styles there can be no props - so the theme is passed as the only argument 92 | (clj->js (camelize-keys (x (js->clj arg :keywordize-keys true)))) 93 | (let [cljs-args (assoc (obj-get arg cljs-props-key) 94 | :theme (js->clj (obj-get arg "theme") :keywordize-keys true))] 95 | ;; invoke the user-supplied function which returns style data - convert whatever they return to js data structures. 96 | (clj->js (camelize-keys (x cljs-args)))))) 97 | 98 | ;; maps come up in value position for nested selectors 99 | (map? x) 100 | (camelize-keys x) 101 | 102 | :else x)))) 103 | 104 | #?(:cljs (goog-define ADD_CLASSNAMES "INITIAL")) 105 | 106 | #?(:cljs 107 | (defn add-class-names? [] 108 | (if (boolean? ADD_CLASSNAMES) 109 | ADD_CLASSNAMES 110 | goog.DEBUG))) 111 | 112 | #?(:cljs 113 | (defn add-class-name [props class-name] 114 | (if (object? props) 115 | (doto props 116 | (goog.object/set "className" 117 | (->> [class-name (goog.object/get props "className")] 118 | (str/join " ") 119 | (str/trim)))) 120 | (update props :className #(if (nil? %) class-name (str class-name " " %)))))) 121 | 122 | #?(:cljs (defn hashit [string] (str "dvcss-" (emotion-hash string)))) 123 | 124 | #?(:cljs 125 | (defn set-class-name [props class-name] 126 | (if class-name 127 | (let [hashed-name (hashit class-name) 128 | props (add-class-name props hashed-name)] 129 | (if (add-class-names?) 130 | (add-class-name props class-name) 131 | props)) 132 | props))) 133 | 134 | #?(:cljs 135 | (defn map->obj [m] 136 | (reduce-kv (fn [o k v] 137 | ;; convert keywords to string only in key position 138 | (let [new-k (cond-> k (implements? INamed k) name) 139 | new-v (cond-> v (map? v) map->obj)] 140 | (doto o (obj-set new-k new-v)))) 141 | #js{} m))) 142 | 143 | #?(:cljs 144 | (defn make-js-props [props class-name] 145 | (if (object? props) 146 | props 147 | (let [clj-props (set-class-name props class-name) 148 | js-props (map->obj clj-props)] 149 | (doto js-props (obj-set cljs-props-key clj-props)))))) 150 | 151 | #?(:cljs 152 | (defn react-factory [el class-name] 153 | (fn 154 | ([] 155 | (jsx* el (clj->js (set-class-name {} class-name)))) 156 | ([props] 157 | (try 158 | (cond 159 | (or (react/isValidElement props) (string? props)) 160 | (jsx* el (set-class-name #js{} class-name) props) 161 | 162 | (map? props) 163 | ;; Do not use clj->js in order to preserve clojure data types like keywords that would not 164 | ;; survive a round-trip clj->js js->clj 165 | (jsx* el (make-js-props props class-name)) 166 | 167 | (object? props) 168 | (jsx* el (set-class-name props class-name)) 169 | 170 | (or (array? props) (coll? props)) 171 | (jsx* el (set-class-name #js{} class-name) (force-children props)) 172 | 173 | :else 174 | (jsx* el (set-class-name #js{} class-name))) 175 | 176 | (catch js/Object e 177 | (js/console.error "Error invoking an emotion styled component: " e)))) 178 | 179 | ([props & children] 180 | ;; if props are a mapping type and not a react child 181 | (if (or (and (object? props) (not (react/isValidElement props))) (map? props)) 182 | (let [js-props (make-js-props props class-name)] 183 | (if (seq children) 184 | (apply jsx* el js-props (force-children children)) 185 | (jsx* el js-props))) 186 | (apply jsx* el (set-class-name #js{} class-name) (force-children (list* props children)))))))) 187 | 188 | #?(:clj 189 | (defn get-type 190 | [styled-arg tag-name] 191 | (cond 192 | ;; if literals, don't need to determine type at runtime 193 | ;; a dom element like :div, same as styled.div`` 194 | (string? tag-name) `(obj-get ~styled-arg ~tag-name) 195 | (keyword? tag-name) `(obj-get ~styled-arg ~(name tag-name)) 196 | :else 197 | `(cond 198 | (string? ~tag-name) 199 | (obj-get ~styled-arg ~tag-name) 200 | 201 | (keyword? ~tag-name) 202 | (obj-get ~styled-arg ~(name tag-name)) 203 | 204 | ;; Another styled component 205 | (::styled (meta ~tag-name)) 206 | (.call ~styled-arg ~styled-arg (::styled (meta ~tag-name))) 207 | 208 | ;; A React component 209 | :else 210 | (.call ~styled-arg ~styled-arg ~tag-name))))) 211 | 212 | #?(:clj 213 | (defn get-cls-name 214 | [namespace-name print-style component-sym] 215 | (case print-style 216 | :full (str namespace-name "/" component-sym) 217 | :short (str component-sym) 218 | :nil nil))) 219 | 220 | #?(:clj (def default-classname-style :full)) 221 | 222 | #?(:clj 223 | (defn get-cls-name-from-meta 224 | "Returns string or nil for the classname" 225 | [namespace-name component-sym] 226 | (if (contains? (meta component-sym) :styled/classname) 227 | (let [print-config (:styled/classname (meta component-sym))] 228 | (if (#{:full :short :nil} print-config) 229 | (get-cls-name namespace-name print-config component-sym) 230 | (throw (Exception. (str "Unknown option for class-name style in metadata passed to component: " component-sym))))) 231 | (get-cls-name namespace-name default-classname-style component-sym)))) 232 | 233 | #?(:clj 234 | (defmacro defstyled 235 | ([component-name el & children] 236 | (let [component-type (gensym "component-type") 237 | clss (gensym "clss") 238 | class-name (gensym "className") 239 | full-class-name (gensym "fullClassName") 240 | children* (gensym "children")] 241 | `(let [~class-name ~(get-cls-name-from-meta (-> &env :ns :name) component-name) 242 | ~full-class-name ~(str (-> &env :ns :name) "/" component-name) 243 | 244 | ~children* 245 | (walk/postwalk 246 | ;; todo here you can do props validation also 247 | ;; should not allow anything that's not a symbol, map, vector, js-obj, js-array, fn 248 | (wrap-call-style-fn false) 249 | ~(vec children)) 250 | 251 | ;; pass js structures to the lib 252 | ~children* (cljs.core/clj->js ~children*) 253 | ~component-type ~(get-type `styled el) 254 | ~clss (.apply ~component-type ~component-type ~children*)] 255 | (obj-set ~clss "displayName" ~(str (-> &env :ns :name) "/" component-name)) 256 | 257 | (def ~component-name 258 | (with-meta (react-factory ~clss ~class-name) 259 | {::styled ~clss 260 | ::hashed-name (hashit ~full-class-name)})) 261 | (cljs.core/specify! ~component-name 262 | ~'Object 263 | (~'toString [this#] 264 | (cljs.core/str "." (::hashed-name (meta ~component-name)))))))))) 265 | 266 | #?(:clj 267 | (comment 268 | (macroexpand-1 '(defstyled button4 button3 {:fontSize "20em"})) 269 | (macroexpand '(defstyled button3 :button 270 | {:background "lightblue" :color "green"})) 271 | 272 | (macroexpand-1 273 | '(defstyled button3 :button 274 | {:background "lightblue" 275 | button1 {:color "green"}} 276 | (fn [jsprops cp] 277 | {:color (or (:color cp) "white")}))) 278 | 279 | ;; postwalk doesn't continue expanding replaced values, like the fn call here: 280 | (walk/postwalk 281 | (fn [i] 282 | (cond 283 | (fn? i) (i) 284 | (keyword? i) (kebab->camel (name i)) 285 | :else i)) 286 | [{:background-color "blue"} 287 | (fn [] {:border-radius 5})]))) 288 | 289 | (comment 290 | (camelize-keys 291 | [{:background "lightblue" 292 | :font-size 20 293 | :border-radius "10px"} 294 | {:background-image "url(xyz.com/my-image)"}]) 295 | (camelize-keys 296 | {:background "lightblue" 297 | :font-size 20 298 | "@media(min-width: 200px)" {:font-size 33} 299 | :border-radius "10px"}) 300 | 301 | (walk/postwalk 302 | (fn [item] 303 | (println "item: " item " map entry: " (map-entry? item) " vec? " (vector? item)) 304 | (cond 305 | (keyword? item) 306 | (do 307 | (println "found keyword") 308 | (keyword (kebab->camel (name item)))) 309 | :elseeeee item) 310 | ) 311 | {:background "lightblue" 312 | :font-size 20 313 | "@media(min-width: 200px)" 314 | [{:font-size 33} 315 | {:background-color "purple"}] 316 | :border-radius "10px"})) 317 | 318 | ;; emotion doesn't allow functions in nested position, only 319 | ;; objects and arrays of objects 320 | ;; but they do allow one function as a child 321 | ;; you can always wrap the call in a fn if you want dynamism like below 322 | ;https://github.com/emotion-js/emotion/blob/188dc0e785cfc9b10b3f9a6ead62b56ddd38e039/packages/core/src/global.js#L16 323 | 324 | #?(:cljs 325 | (defn global-style 326 | "Takes a cljs vector or hashmap of styles and converts to JS types before calling emotion's Global function 327 | from the @emotion/react package. 328 | Inserts styles into the DOM that target global elements and classes. 329 | 330 | https://emotion.sh/docs/@emotion/react" 331 | [props] 332 | (react/createElement Global #js{:styles (clj->js (camelize-keys props))}))) 333 | 334 | ;; can use like so: 335 | (comment 336 | (global-style {:body {:background "#cce" "@media (min-width:700px)" {:background "white"}}}) 337 | (global-style 338 | [(clj->js {:body {:font-family "serif"}}) {:body {:border "2px solid yellOW"}} {:body {:background-color "#ecccee"}}]) 339 | 340 | ;; to adapt based on props, wrap in a fn: 341 | (defn my-globals [props] 342 | (global-style 343 | {:body {:background-color "red"}}))) 344 | 345 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 346 | ;; Theme support 347 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 348 | 349 | #?(:cljs 350 | (defn theme-provider 351 | "Takes a hashmap of a style theme and react children to render with that theme in the React Context 352 | using emotion's ThemeProvider." 353 | [props & children] 354 | (when-not (contains? props :theme) (throw (js/Error. "You must pass a :theme to the theme-provider."))) 355 | (if (:theme props) 356 | (apply react/createElement ThemeProvider 357 | (clj->js props) 358 | (force-children children)) 359 | (react/Children.map children (fn [c i] (react/cloneElement c #js{:key i})))))) 360 | 361 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 362 | ;; CSS prop support 363 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 364 | 365 | #?(:clj 366 | (defn ^:private css-body [props] 367 | `(do 368 | ;(assert (contains? ~props :css) "Props must contain :css key") 369 | (cljs.core/clj->js 370 | (assoc ~props :css 371 | (walk/postwalk 372 | ;; todo here you can do props validation also 373 | ;; should not allow anything that's not a symbol, map, vector, js-obj, js-array, fn 374 | (wrap-call-style-fn true) 375 | (:css ~props))))))) 376 | #?(:clj 377 | (defmacro jsx 378 | ([el props] 379 | (let [el (cond-> el (keyword? el) name) 380 | css-props (css-body props)] 381 | `(jsx* ~el ~css-props))) 382 | 383 | ([el props & children] 384 | (let [el (cond-> el (keyword? el) name) 385 | css-props (css-body props)] 386 | `(jsx* ~el ~css-props ~@children))))) 387 | 388 | #?(:cljs 389 | (defn convert-css 390 | "Takes a hashmap or vector of styles and converts to JS types, will pass any functions cljs data structures." 391 | [css] 392 | (clj->js (walk/postwalk (wrap-call-style-fn true) css)))) 393 | 394 | #?(:cljs 395 | (defn css [& css] 396 | (styled-core/css (clj->js (walk/postwalk (wrap-call-style-fn true) css))))) 397 | -------------------------------------------------------------------------------- /src/dev/dv/cljs_emotion/devcards.cljs: -------------------------------------------------------------------------------- 1 | (ns dv.cljs-emotion.devcards 2 | (:require 3 | [devcards.core :as dc :refer (defcard)] 4 | [dv.cljs-emotion.fulcro-cards] 5 | [dv.cljs-emotion.reagent-cards] 6 | [dv.cljs-emotion.reagent-debug] 7 | [dv.cljs-emotion.debug] 8 | [dv.cljs-emotion.helix-cards] 9 | [dv.cljs-emotion.target-styled] 10 | [sablono.core :as sab :refer [html]] 11 | ["polished" :as p :refer [darken]] 12 | [dv.cljs-emotion :as em :refer [jsx defstyled keyframes global-style theme-provider]])) 13 | 14 | (enable-console-print!) 15 | 16 | (defcard 17 | " 18 | Hi there. This page uses devcards to demonstrate usage of the dv.cljs-emotion library. 19 | This library is a small wrapper around the emotion css-in-js JavaScript library that 20 | makes it easy to use all of CSS within a react application. 21 | 22 | The source of these cards can be found here: 23 | https://github.com/dvingo/cljs-emotion/tree/master/src/dev/dv/cljs_emotion 24 | 25 | The main API of this library is: `[defstyled keyframes global-style theme-provider]` 26 | 27 | defstyled is a wrapper around `@emotion/styled` 28 | Styled is multi-arity and supports passing functions that return maps of styles, a vector of styles that 29 | will be merged top down (first to last) and a map, you can also pass JavaScript objects and arrays. 30 | 31 | This library converts between cljs and js structures, but the multi-arity capability is built into emotion already. 32 | ") 33 | 34 | (defcard 35 | "# Use it 36 | 37 | Require the library: 38 | ```clojure 39 | (require [dv.cljs-emotion :refer [defstyled keyframes global-style theme-provider]]) 40 | ``` 41 | 42 | Or for reagent support: 43 | ```clojure 44 | (require [dv.cljs-emotion-reagent :refer [defstyled keyframes global-style theme-provider]]) 45 | ``` 46 | 47 | You can pass any number of children to defstyled: 48 | 49 | ```clojure 50 | (defstyled sample1 :div 51 | {:background-color \"RebeccaPurple\"}) 52 | ``` 53 | 54 | ```clojure 55 | (defstyled sample1 :div 56 | {:background-color \"RebeccaPurple\"} 57 | #js{:color \"yellow\"} 58 | (fn [props] {:border-radius 4})) 59 | ``` 60 | 61 | This library delegates to emotion's styled API, while adding a transform layer for cljs->js data structures. 62 | " 63 | ) 64 | 65 | (def global-data (atom {:on? false})) 66 | 67 | (def btn-styles 68 | {"button" 69 | {:background-color "#f2f2f2" 70 | :background-image "linear-gradient(to bottom, #f2f2f2, #f2f2f2)" 71 | :border "1px solid #bfbfbf" 72 | :text-shadow "0 1px 0 rgba(255, 255, 255, 0.5)" 73 | :border-radius 3 74 | :cursor "pointer" 75 | :color "#8c8c8c" 76 | :font-family "sans-serif" 77 | :line-height "1rem" 78 | :font-weight 700 79 | :margin "16px 0 0 16px" 80 | :padding "9px 16px 9px" 81 | :transition "all 20ms ease-out" 82 | :vertical-align "top" 83 | :box-shadow (str "inset 0 1px 0 white, inset 0 -1px 0 #d9d9d9," 84 | " inset 0 0 0 1px #f2f2f2, 0 2px 4px rgba(0, 0, 0, 0.2)") 85 | ":active" {:box-shadow "inset 0 2px 3px rgba(0, 0, 0, 0.2)"} 86 | ":hover" {:background "#f2f2f2" 87 | :border-color "#8c8c8c" 88 | :box-shadow "inset 0 1px 0 white, inset 0 -1px 0 #d9d9d9, inset 0 0 0 1px #f2f2f2"}}}) 89 | 90 | (def button-styles 91 | {"button" 92 | {:padding "1rem 2rem" 93 | :border-radius "4px" 94 | :transition "all .5s" 95 | ":hover" {:box-shadow " 0px 10px 10px rgba(0,0,0,0.2)" 96 | :transform "translateY(-3px)" 97 | "::after" {:transform "scaleX(1.4) scaleY(1.6)" 98 | :opacity 0}} 99 | "::after" {:content "''" 100 | :text-decoration "none" 101 | :text-transform "uppercase" 102 | :position "absolute" 103 | :width "100 %" 104 | :height "100 %" 105 | :top 0 106 | :left 0 107 | :border-radius 100 108 | :display "inline-block" 109 | :z-index -1 110 | :transition "all .5s"}}}) 111 | 112 | (defcard 113 | "For a quick demonstration we will change some global styles on this page. 114 | 115 | In this example we add some styles to `