├── .gitignore ├── LICENSE ├── Makefile ├── RATIONALE.md ├── README.md ├── bin └── test_cljs.sh ├── circle.yml ├── project.clj ├── resources └── public │ └── index.html ├── scripts ├── figwheel.clj └── test_cljs.clj └── src ├── devcards └── om_css │ └── devcards │ ├── bugs.cljs │ ├── constants.cljc │ ├── core.cljs │ └── sablono.cljs ├── main └── om_css │ ├── core.cljc │ ├── dom.cljc │ ├── output_css.clj │ └── utils.cljc └── test └── om_css ├── runner.cljs ├── tests.clj └── tests.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | /out 3 | /target 4 | /classes 5 | /checkouts 6 | pom.xml.asc 7 | *.map 8 | *.js 9 | *.jar 10 | *.class 11 | /.lein-* 12 | /.nrepl-port 13 | *init.clj 14 | .repl* 15 | node_modules 16 | .cljs_node_repl 17 | *.log 18 | resources/public/main.css 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | lein test 3 | lein with-profile +client-test doo node test once 4 | -------------------------------------------------------------------------------- /RATIONALE.md: -------------------------------------------------------------------------------- 1 | ## High level ideas 2 | - CSS/SASS is not a good programming language and composition, reuse, and 3 | abstraction should happen in clojure side. 4 | - A large CSS codebase is very difficult to reason about and CSS overrides are very difficult to 5 | reason about 6 | - Components are a great way to do composition, reuse, etc 7 | 8 | ## Concrete idea 9 | - Write CSS that is one to one with components. Basically CSS rules that only apply to your 10 | component and nothing else. 11 | 12 | ## Implementation Idea 13 | - Each css rule should be prefixed by a namespace path and the component name. 14 | 15 | ```clojure 16 | (defui Foo 17 | Object 18 | (render [this] 19 | (dom/div {:class "ladder_components_Foo"} 20 | (dom/div {:class "ladder_components_Foo--section"} "section")))) 21 | ``` 22 | 23 | ```css 24 | ladder_components_Foo { 25 | some: style; 26 | } 27 | 28 | ladder_components_Foo--section { 29 | more: style; 30 | } 31 | ``` 32 | 33 | ## Convenience / syntactic sugar 34 | 35 | - Writing these full qualified names is prone to mistakes and is tedious. 36 | - We want to be able to enforce a correspondence between style selectors and component class names. 37 | - We also want to collocate style with components 38 | - CSS in CLJS library https://github.com/noprompt/garden 39 | 40 | Ultimately we want to get to something like this 41 | 42 | ```clojure 43 | (ns ladder.components 44 | (:require [ladder.css :as css])) 45 | 46 | (defui Foo 47 | Style 48 | (style [] 49 | [[:.root {:color (:blue css)}] 50 | [:.section (merge css/default-section 51 | {:background-color :green})]]) 52 | Object 53 | (render [this] 54 | (dom/div {:class :root} 55 | (dom/div {:class :section} "section")) 56 | ``` 57 | 58 | 59 | ## Annoyances 60 | 61 | - We'll want to run our css through https://github.com/postcss/autoprefixer 62 | - This is a js library 63 | - Solutions: Compiler could run cljs in nodejs 64 | - Solutions: We could setup / hit autoprefixer from clojure over HTTP 65 | - Solutions: We could output a css file and have webpack compile it 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Om-css [![Circle CI](https://circleci.com/gh/ladderlife/om-css.svg?style=svg)](https://circleci.com/gh/ladderlife/om-css) 2 | 3 | Colocated CSS in Om Next components. 4 | 5 | ## Contents 6 | 7 | - [Installation](#installation) 8 | - [Guide](#guide) 9 | - [Getting started](#getting-started) 10 | - [Usage with boot](#usage-with-boot) 11 | - [Defining components](#defining-components) 12 | - [`defui`](#defui) 13 | - [`defcomponent`](#defcomponent) 14 | - [Referring to global classes](#referring-to-global-classes) 15 | - [User-defined variables in colocated styles](#user-defined-variables-in-colocated-styles) 16 | - [Copyright & License](#copyright--license) 17 | 18 | 19 | ## Installation 20 | 21 | Add Om-css to your dependencies: 22 | 23 | [![Clojars Project](https://clojars.org/com.ladderlife/om-css/latest-version.svg)](https://clojars.org/com.ladderlife/om-css) 24 | 25 | ## Guide 26 | 27 | ### Getting started 28 | 29 | To get started, `:require` Om-css in your project: 30 | 31 | ```clojure 32 | (ns my-ns.core 33 | (:require [om-css.core :as oc :refer-macros [defui]] 34 | [om-css.dom :as dom]) 35 | ``` 36 | 37 | **Note**: In order to use the colocated style capabilities provided by Om-css, using its own `defui` and `om-css.dom` is required over Om's. 38 | 39 | Om-css provides a way to collocate CSS styles in components. However, this alone is not enough to get actual stylesheet files that you can link to in your web pages. Hence, Om-css will generate such css file for you. By default, the generated file will be called `out.css` and will be output at the root of your project. You can, however, instruct Om-css to output to a particular file. Simply add a `:css-output-to` option to the ClojureScript compiler options. Below is an example. [Here](./scripts/figwheel.clj#L15)'s a real example. 40 | 41 | ```clojure 42 | :compiler {:main 'om-css-example.core 43 | :asset-path "out" 44 | :output-to "resources/public/main.js" 45 | :output-dir "resources/public/out" 46 | :source-map true 47 | :optimizations :none 48 | :css-output-to "resources/public/main.css"} 49 | ``` 50 | 51 | #### Usage with boot 52 | 53 | The `:css-output-to` option of om-css is a bit of a mismatch with boot, because boot will write to an new folder after every cljs compile. The solution here is to not set the `:css-output-to` option. Then om-css will write the css file in the same place as your cljs-output. So let's say you have a `public/js/main.cljs.edn` file then boot-cljs will write to `public/js/main.js` and om-css will write to `public/js/main.outout.css`. If you would like a nicer css filename you can use the `sift` task of boot for the preceding case you could use it like this: 54 | 55 | ```clojure 56 | (deftask compile-cljs-and-css 57 | (comp 58 | (cljs :ids #{"public/js/main"}) 59 | (sift :move {#"^public\/js\/main\.outout\.css$" "public/css/next.css"}))) 60 | ``` 61 | 62 | ### Defining components 63 | 64 | #### `defui` 65 | 66 | Components are defined as in Om Next. In addition, Om-css provides the `Style` protocol, which you must implement in order to get all the functionality Om-css provides. This protocol consists of a single function, `style`, which must return a [Garden](https://github.com/noprompt/garden) styles vector. 67 | 68 | In the example shown below, we implement a simple component that declares a style consisting of a single class, `:root`. In the component's `render` function, the props passed to React elements need not be JavaScript objects, and may instead be regular Clojure(Script) maps. The `:class` prop is special to Om-css in the sense that it will be prefixed with the namespace and component name so that there are no clashes between components that declare classes with the same names. In our simple example, `:root` will be transformed to `"my_ns_core_SimpleComponent_root"`, and Om-css will generate CSS with the same class name. 69 | 70 | ```clojure 71 | (ns my-ns.core) 72 | 73 | (defui SimpleComponent 74 | oc/Style 75 | (style [_] 76 | [[:.root {:color :yellow}]]) 77 | Object 78 | (render [this] 79 | (dom/div {:class :root} ;; <= use a vector `[:one :two]` to add multiple classes to an element 80 | "Div with class :root")) 81 | ``` 82 | 83 | #### `defcomponent` 84 | 85 | `defcomponent` is syntactic sugar for simple React elements. These can optionally include a Garden styles vector in their implementation, before the element implementation itself. The following example demonstrates a `defcomponent` implementation. 86 | 87 | ```clojure 88 | (defcomponent element-with-style [props children] 89 | [[:.example-class {:background-color "tomato"}]] ;; <= optional 90 | (dom/div {:class :example-class} 91 | "Nested `defcomponent` example")) 92 | ``` 93 | 94 | ### Referring to global classes 95 | 96 | Collocating CSS within components is not enough for every use case. At times, you may want to use a global CSS class that is defined somewhere else. Referring to classes defined in another location is possible both in Om-css's styles vector and in the components implementation. 97 | 98 | To reference externally defined CSS classes in the colocated styles, simply use the `$` prefix instead of the normal CSS `.` prefix. To do so in the components `render` implementation, use either one of `:className` or `:class-name`. Om-css will only prefix classes that appear in the `:class` prop. 99 | 100 | The following example shows how this is done in practice. The CSS generated by the styles vector of `example-component` is what you might expect and is presented below the component implementation. 101 | 102 | ```clojure 103 | (ns my-ns.core) 104 | 105 | (defcomponent example-component [props children] 106 | [:$desktop 107 | [:.root {:background-color "tomato"}]] 108 | (dom/div {:className "desktop"} 109 | (dom/div {:class :root} 110 | "Some text"))) 111 | ``` 112 | 113 | ```css 114 | .desktop .my_ns_core_example-component_root { 115 | background-color: tomato; 116 | } 117 | ``` 118 | 119 | ### User-defined variables in colocated styles 120 | 121 | Om-css compiles and generates the CSS at macro-expansion time. Because ClojureScript macros are written in Clojure, any functions or variables used inside the colocated style must be declared in a `.clj` or `.cljc` file (commonly a `.cljc` file is preferred so that you can also refer to those variables in your ClojureScript code). An example is presented below. 122 | 123 | ```clojure 124 | (ns my-ns.constants) 125 | 126 | (def some-style {:margin "0px"}) 127 | 128 | (ns my-ns.core 129 | (:require [om-css.core :as oc :refer-macros [defui]] 130 | [om-css.dom :as dom] 131 | [my-ns.constants :as c])) 132 | 133 | (defui Foo 134 | static oc/Style 135 | (style [_] 136 | [[:.root {:background-color "tomato"}] 137 | [:.section (merge c/some-style ;; <= notice this 138 | {:background-color :green})]]) 139 | Object 140 | (render [this] 141 | (dom/div {:class :root} "div with class :root" 142 | (dom/section {:class :section} "section with class :section")))) 143 | ``` 144 | 145 | Check out more examples [here](./src/devcards/om_css/devcards/core.cljs). 146 | 147 | 148 | ## Copyright & License 149 | 150 | Copyright © 2016 Ladder Financial, Inc. 151 | 152 | Distributed under the Eclipse Public License (see [LICENSE](./LICENSE)). 153 | -------------------------------------------------------------------------------- /bin/test_cljs.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | lein trampoline run -m clojure.main scripts/test_cljs.clj 4 | node target/test/test.js 5 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | java: 3 | version: oraclejdk8 4 | 5 | dependencies: 6 | override: 7 | - lein with-profiles +client-test deps 8 | 9 | test: 10 | override: 11 | - make test 12 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.ladderlife/om-css "0.6.1-SNAPSHOT" 2 | :description "Colocated CSS in Om Next components" 3 | :url "http://github.com/ladderlife/om-css" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :repositories [["clojars" {:sign-releases false}]] 7 | :dependencies [[org.clojure/clojure "1.9.0-alpha10" :scope "provided"] 8 | [org.clojure/clojurescript "1.9.216" :scope "provided"] 9 | [org.omcljs/om "1.0.0-alpha41" :scope "provided"] 10 | 11 | [com.ladderlife/cellophane "0.3.5"] 12 | [garden "1.3.2"] 13 | 14 | [figwheel-sidecar "0.5.4-7" :scope "test"] 15 | [devcards "0.2.1-7" :scope "test"] 16 | [devcards-om-next "0.3.0" :scope "test"]] 17 | :profiles {:client-test {:dependencies [[cljsjs/react "15.3.0-0"]] 18 | :plugins [[lein-doo "0.1.7"] 19 | [lein-cljsbuild "1.1.3"]] 20 | :cljsbuild {:builds [{:id "test" 21 | :source-paths ["src/main" "src/test"] 22 | :compiler {:output-to "target/js/client_test.js" 23 | :output-dir "target/js/out" 24 | :main om-css.runner 25 | :target :nodejs 26 | :optimizations :none}}]}} 27 | :dev {:dependencies [[sablono "0.7.4"] 28 | [com.cemerick/piggieback "0.2.1" 29 | :exclusions [org.clojure/clojurescript]]]}} 30 | :jvm-opts ^:replace ["-Xmx1g" "-server"] 31 | :jar-exclusions [#"test" #"devcards" #"public" #"runner"] 32 | :source-paths ["src/main" "src/devcards" "src/test"] 33 | :test-paths ["src/test"] 34 | :clean-targets ^{:protect false} ["target" 35 | "resources/public/out" 36 | "resources/public/main.js"] 37 | :target-path "target") 38 | -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | om-css 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /scripts/figwheel.clj: -------------------------------------------------------------------------------- 1 | (require '[figwheel-sidecar.repl :as r] 2 | '[figwheel-sidecar.repl-api :as ra]) 3 | 4 | (ra/start-figwheel! 5 | {:figwheel-options {:css-dirs ["resources/public"] 6 | :validate-config false 7 | :validate-interactive false} 8 | :build-ids ["devcards"] 9 | :all-builds 10 | [{:id "devcards" 11 | :figwheel {:devcards true} 12 | :source-paths ["src/main" "src/devcards"] 13 | :compiler {:main 'om-css.devcards.core 14 | :asset-path "/out" 15 | :output-to "resources/public/main.js" 16 | :output-dir "resources/public/out" 17 | :css-output-to "resources/public/main.css" 18 | :parallel-build true 19 | :compiler-stats true}}]}) 20 | 21 | (ra/cljs-repl) 22 | -------------------------------------------------------------------------------- /scripts/test_cljs.clj: -------------------------------------------------------------------------------- 1 | (require '[cljs.build.api :as b]) 2 | 3 | (b/build (b/inputs "src/main" "src/test") 4 | {:target :nodejs 5 | :main 'om-css.runner 6 | :output-to "target/test/test.js" 7 | :output-dir "target/test/out" 8 | :parallel-build true 9 | :compiler-stats true 10 | :static-fns true 11 | :optimizations :none}) 12 | 13 | 14 | (System/exit 0) 15 | -------------------------------------------------------------------------------- /src/devcards/om_css/devcards/bugs.cljs: -------------------------------------------------------------------------------- 1 | (ns om-css.devcards.bugs 2 | (:require-macros [devcards.core :as dc :refer [defcard deftest]] 3 | [cljs.test :refer [is testing async]]) 4 | (:require [devcards-om-next.core :as don :refer-macros [defcard-om-next]] 5 | [goog.dom :as gdom] 6 | [om.next :as om] 7 | [om.dom :as om-dom] 8 | [om-css.dom :as dom] 9 | [om-css.core :as oc :refer-macros [defui defcomponent]] 10 | [om-css.devcards.constants :as c :refer [other-style]])) 11 | 12 | ;;==================== 13 | ;; OMCSS-2 14 | 15 | (defcomponent omcss-2-component [{:keys [a b] :as props} children] 16 | (dom/div nil 17 | (dom/p nil (str "prop a: " a)) 18 | (dom/p nil (str "prop b: " b)))) 19 | 20 | (defcard omcss-2-card 21 | "Test that destructuring works in defcomponent's args" 22 | (omcss-2-component {:a 1 :b 2})) 23 | 24 | ;;==================== 25 | ;; OMCSS-4 26 | 27 | (defcomponent omcss-4-component 28 | [props children] 29 | [[:.root {:color :purple}] 30 | [:.active {:font-weight :bold}] 31 | [:.section {:color :green}]] 32 | (let [x true] 33 | (dom/div 34 | {:class [:root :active]} 35 | "div with class root" 36 | (dom/hr) 37 | (dom/section {:class :section} 38 | "section with class :section" 39 | children)))) 40 | 41 | (defcard omcss-4-card 42 | "Test that `let` expressions work in `defcomponent`" 43 | (omcss-4-component)) 44 | 45 | ;;==================== 46 | ;; OMCSS-3 47 | 48 | (defcomponent omcss-3-component [props children] 49 | [[:.root {:color :red}]] 50 | (dom/div {:class :root :class-name "inline-block"} 51 | "test")) 52 | 53 | (defcard omcss-3-card 54 | "Test that merging with regular class names works" 55 | (omcss-3-component)) 56 | 57 | ;;==================== 58 | ;; OMCSS-5 59 | 60 | (defcomponent omcss-5-component [props children] 61 | [:$desktop 62 | [:.root {:background-color "tomato"}]] 63 | (dom/div {:className "desktop"} 64 | (dom/div {:class :root} 65 | "test"))) 66 | 67 | (defcard omcss-5-card 68 | "Test that referencing global class names works" 69 | (omcss-5-component)) 70 | 71 | ;;==================== 72 | ;; OMCSS-7 73 | 74 | (defcomponent omcss-7-component-1 75 | [{:keys [class] :as props} children] 76 | [:.root {:color :purple}] 77 | (dom/div 78 | {:id "omcss-7" :class (into class [:root])} 79 | "class is now [:root :root], parent's class is lost")) 80 | 81 | (defcomponent omcss-7component-2 82 | [props children] 83 | [:.root {:text-decoration :underline}] 84 | (omcss-7-component-1 {:class [:root]})) 85 | 86 | (defcard omcss-7-card 87 | "Test that assigning classes to child components works" 88 | (omcss-7component-2)) 89 | 90 | (deftest omcss-7-test 91 | (let [c (gdom/getElement "omcss-7") 92 | cns (.-className c) 93 | cns (.split cns " ")] 94 | (is (not (nil? c))) 95 | (is (not (nil? 96 | (some 97 | #{"om_css_devcards_bugs_omcss-7component-2_root"} 98 | cns)))) 99 | (is (not (nil? 100 | (some 101 | #{"om_css_devcards_bugs_omcss-7-component-1_root"} 102 | cns)))))) 103 | 104 | ;;==================== 105 | ;; OMCSS-8 106 | 107 | (defcomponent omcss-8-component [props children] 108 | (om-dom/div #js {:className "test"} "test")) 109 | 110 | (defcard omcss-8-card 111 | (omcss-8-component)) 112 | 113 | ;;==================== 114 | ;; OMCSS-11 115 | 116 | (defcomponent omcss-11-component [props children] 117 | [[:.root {:color :purple}]] 118 | (dom/div 119 | (merge props {:class :root}) 120 | "purple")) 121 | 122 | (defcard omcss-11-card 123 | (omcss-11-component)) 124 | 125 | ;;==================== 126 | ;; OMCSS-12 127 | 128 | (defui ^:once OMCSS-12-Defui 129 | static oc/Style 130 | (style [_] 131 | [[:.root {:background-color "tomato"}] 132 | [:.section (merge c/some-style 133 | {:background-color :green})]]) 134 | Object 135 | (render [this] 136 | (dom/div {:class :root} "div with class :root" 137 | (dom/section {:class :section} "section with class :section")))) 138 | 139 | (defcard-om-next omcss-12-defui-card 140 | OMCSS-12-Defui) 141 | 142 | (defcomponent OMCSS-12-Defcomponent [props children] 143 | [[:.root {:background-color "tomato"}] 144 | [:.section (merge c/some-style other-style 145 | {:background-color :green})]] 146 | (dom/div {:class :root} "div with class :root" 147 | (dom/section {:class :section} "section with class :section"))) 148 | 149 | (defcard omcss-12-defcomponent-card 150 | OMCSS-12-Defcomponent) 151 | 152 | 153 | ;;==================== 154 | ;; OMCSS-18 155 | 156 | (defn omcss-18-add-classes [{:keys [class] :as props} & classes] 157 | (merge props {:class (flatten [class classes])})) 158 | 159 | (defcomponent omcss-18-component [{:keys [class] :as props} children] 160 | [[:.test {:color :test}]] 161 | (dom/div (omcss-18-add-classes props :test) children)) 162 | 163 | (defcard omcss-18-card 164 | (omcss-18-component {:id "omcss-18"} "test")) 165 | 166 | (deftest omcss-18-test 167 | (let [c (gdom/getElement "omcss-18") 168 | cns (.-className c) 169 | cns (.split cns " ")] 170 | (is (not (nil? c))) 171 | (is (not (nil? 172 | (some 173 | #{"om_css_devcards_bugs_omcss-18-component_test"} 174 | cns)))))) 175 | 176 | ;;==================== 177 | ;; OMCSS-20 178 | 179 | (defcomponent omcss-20-component [props children] 180 | (let [dir "even"] 181 | (dom/div 182 | {:class (if (= dir "even") [:even] [])} 183 | "asd"))) 184 | 185 | (defcard omcss-20-card 186 | (omcss-20-component)) 187 | 188 | ;;==================== 189 | ;; OMCSS-23 190 | 191 | (defcomponent inner [props children] 192 | (dom/div props children)) 193 | 194 | (defcomponent outer [props children] 195 | (dom/div props children)) 196 | 197 | (defcomponent wrapper [props children] 198 | [[:.inner {}]] 199 | (let [] 200 | (outer {:class :outer 201 | :id "omcss-23"} 202 | (inner {:class :inner} "inner")))) 203 | 204 | (defcard omcss-23-card 205 | (wrapper)) 206 | 207 | (deftest omcss-23-test 208 | (let [c (gdom/getElement "omcss-23") 209 | cns (.-className c) 210 | cns (.split cns " ")] 211 | (is (not (nil? c))) 212 | (is (not (nil? (some #{"outer"} cns)))))) 213 | 214 | ;;==================== 215 | ;; OMCSS-22 216 | 217 | (defcomponent omcss-22 [props children] 218 | [] 219 | [(dom/div nil "something") (dom/div "other")]) 220 | 221 | (defcard omcss-22-card 222 | (dom/div nil 223 | (omcss-22))) 224 | 225 | ;;==================== 226 | ;; OMCSS-25 227 | 228 | (defcomponent omcss-25-component [props children] 229 | (dom/div {:class-name nil} "omcss-25")) 230 | 231 | (defcard omcss-25-card 232 | (omcss-25-component)) 233 | 234 | ;;==================== 235 | ;; nested-fns in components 236 | 237 | (defcomponent nested-fn-component [_ _] 238 | [[:hi {:text-align "center"}]] 239 | (dom/div nil 240 | "something" 241 | (map-indexed 242 | (fn [index _] 243 | (dom/p {:class :hi} (str "index: " index))) 244 | [1 2 3 4]))) 245 | 246 | (defcard nested-fn-card 247 | (nested-fn-component)) 248 | 249 | ;;==================== 250 | ;; OMCSS-32 251 | 252 | (defui OMCSS-32-Component 253 | Object 254 | (render [this] 255 | (dom/div {:ref :some-ref} "div with ref"))) 256 | 257 | (def omcss-32-reconciler 258 | (om/reconciler {:state (atom nil) 259 | :parser (om/parser {:read #(do {})})})) 260 | 261 | (defcard-om-next omcss-32-card 262 | OMCSS-32-Component 263 | omcss-32-reconciler) 264 | 265 | (deftest test-omcss-32 266 | (let [c (om/class->any omcss-32-reconciler OMCSS-32-Component)] 267 | (is (some? c)) 268 | (is (some? (om/react-ref c :some-ref))))) 269 | 270 | ;;==================== 271 | ;; Test that #js {} as props works 272 | 273 | (defui JSObjsComponent 274 | Object 275 | (render [this] 276 | (dom/div #js {:id "js-obj-comp"} "I work"))) 277 | 278 | (defcard-om-next js-objs-card 279 | JSObjsComponent) 280 | 281 | 282 | ;;==================== 283 | ;; no protocol method -assoc for [object Object] 284 | 285 | (defcomponent no-protocol-defc [props children] 286 | (dom/div props children)) 287 | 288 | (defcard no-protocol-card 289 | (no-protocol-defc 290 | (dom/div {:id "no-protocol-comp"} "some text"))) 291 | 292 | (deftest test-no-protocol-for-object 293 | (let [c (gdom/getElement "no-protocol-comp")] 294 | (is (some? c)))) 295 | -------------------------------------------------------------------------------- /src/devcards/om_css/devcards/constants.cljc: -------------------------------------------------------------------------------- 1 | (ns om-css.devcards.constants) 2 | 3 | (def some-style 4 | {:color :yellow}) 5 | 6 | (def other-style 7 | {:text-decoration :underline}) 8 | -------------------------------------------------------------------------------- /src/devcards/om_css/devcards/core.cljs: -------------------------------------------------------------------------------- 1 | (ns om-css.devcards.core 2 | (:require-macros [devcards.core :as dc :refer [defcard deftest]] 3 | [cljs.test :refer [is testing async]]) 4 | (:require [devcards-om-next.core :as don :refer-macros [defcard-om-next]] 5 | [goog.dom :as gdom] 6 | [om.next :as om] 7 | [om-css.dom :as dom] 8 | [om-css.core :as oc :refer-macros [defui defcomponent]] 9 | [om-css.devcards.bugs] 10 | [om-css.devcards.sablono])) 11 | 12 | (def style-1 13 | {:text-align :center}) 14 | 15 | (defui Foo 16 | static oc/Style 17 | (style [_] 18 | [[:.root {:background-color "tomato"}] 19 | [:.section (merge {} ;;style-1 20 | {:background-color :green})]]) 21 | Object 22 | (render [this] 23 | (dom/div {:id "ns-test"} 24 | (dom/div {:class :root} "div with class :root" 25 | (dom/section {:class :section} "section with class :section" 26 | (dom/p {:className "preserved" 27 | :style {:background-color "turquoise"}} "paragraph with class \"preserved\"")))))) 28 | 29 | (defcard-om-next foo-card 30 | Foo) 31 | 32 | (deftest namespaced-classnames-in-dom 33 | (testing "classnames are namespace qualified" 34 | (is (not (nil? (gdom/getElement "ns-test")))) 35 | (is (not (nil? (gdom/getElementByClass "om_css_devcards_core_Foo_root")))) 36 | (is (not (nil? (gdom/getElementByClass "om_css_devcards_core_Foo_section")))) 37 | (is (not (nil? (gdom/getElementByClass "preserved")))))) 38 | 39 | (defui Bar 40 | oc/Style 41 | (style [_] 42 | [[:.bar {:margin "0 auto"}] 43 | [:.other {:padding "0"}]]) 44 | Object 45 | (render [this] 46 | (dom/div {:class :bar} "Bar component"))) 47 | 48 | (defcard-om-next bar-card 49 | Bar) 50 | 51 | (defui ComponentWithoutStyle 52 | Object 53 | (render [this] 54 | (dom/div {:id "component-no-style"} 55 | "Component Without Style."))) 56 | 57 | (defcard-om-next card-component-no-style 58 | ComponentWithoutStyle) 59 | 60 | (defui NotStaticStyleComponent 61 | oc/Style 62 | (style [this] 63 | {:.somestyle {:background-color "red"}}) 64 | Object 65 | (render [this] 66 | (dom/div {:class :somestyle} 67 | "Component that implements Style (non-static)"))) 68 | 69 | (defcard-om-next card-component-no-static-style 70 | "Test that Style doesn't need to appear with `static`" 71 | NotStaticStyleComponent) 72 | 73 | (defcomponent defcomponent-example [props children] 74 | [[:.defcomponent-class {:color "green"}]] 75 | (dom/div {:class :defcomponent-class} 76 | "`defcomponent` example with class `:defcomponent-class`")) 77 | 78 | (defcard defcomponent-example-card 79 | (defcomponent-example)) 80 | 81 | (defcomponent nested-defcomponent-example [props children] 82 | (dom/div {:id "nested-defcomponent" :class :nested-defcomponent} 83 | "Nested `defcomponent` example" 84 | (defcomponent-example {:class :some} 85 | "some text"))) 86 | 87 | (defcard nested-defcomponent-example-card 88 | (nested-defcomponent-example)) 89 | 90 | (defcomponent defcomponent-with-style [props children] 91 | [[:.example-class {:background-color "tomato"}]] 92 | (dom/div {:class :example-class} 93 | "Nested `defcomponent` example" 94 | (defcomponent-example {:class :some} 95 | "some text"))) 96 | 97 | (defcard defcomponent-with-style-card 98 | (defcomponent-with-style)) 99 | 100 | (deftest namespaced-classnames-in-defcomponent 101 | (testing "`defcomponent` with styles" 102 | (is (not (nil? (gdom/getElementByClass "om_css_devcards_core_defcomponent-example_defcomponent-class")))) 103 | (is (not (nil? (gdom/getElementByClass "om_css_devcards_core_defcomponent-with-style_example-class")))))) 104 | 105 | (defui MultipleClassesDefui 106 | static oc/Style 107 | (style [_] 108 | [[:.some {:background-color "tomato"}] 109 | [:.other {:color "yellow"}]]) 110 | Object 111 | (render [this] 112 | (dom/div {:id "multiple-classes-test-defui" 113 | :class [:some :other]} 114 | "div with classes [:some :other]"))) 115 | 116 | (defcard-om-next defui-multiple-classes 117 | "Render a `defui` component with multiple classes" 118 | MultipleClassesDefui) 119 | 120 | (deftest multiple-classnames-in-defui 121 | (testing "`defcomponent` with styles" 122 | (let [c (gdom/getElement "multiple-classes-test-defui") 123 | cns (.-className c) 124 | cns (.split cns " ")] 125 | (is (not (nil? c))) 126 | (is (= (count cns) 2)) 127 | (is (= (first cns) "om_css_devcards_core_MultipleClassesDefui_some")) 128 | (is (= (second cns) "om_css_devcards_core_MultipleClassesDefui_other"))))) 129 | 130 | (defcomponent MultipleClassesDefcomponent [props children] 131 | [[:.some {:background-color "tomato"}] 132 | [:.other {:color "yellow"}]] 133 | (dom/div {:id "multiple-classes-test-defcomponent" 134 | :class [:some :other]} 135 | "div with classes [:some :other]" 136 | children)) 137 | 138 | (defcard defcomponent-multiple-classes 139 | "Render a `defcomponent` component with multiple classes" 140 | (MultipleClassesDefcomponent nil 141 | (dom/div nil "child"))) 142 | 143 | (deftest multiple-classnames-in-defcomponent 144 | (testing "`defcomponent` with styles" 145 | (let [c (gdom/getElement "multiple-classes-test-defcomponent") 146 | cns (.-className c) 147 | cns (.split cns " ")] 148 | (is (not (nil? c))) 149 | (is (= (count cns) 2)) 150 | (is (= (first cns) "om_css_devcards_core_MultipleClassesDefcomponent_some")) 151 | (is (= (second cns) "om_css_devcards_core_MultipleClassesDefcomponent_other"))))) 152 | -------------------------------------------------------------------------------- /src/devcards/om_css/devcards/sablono.cljs: -------------------------------------------------------------------------------- 1 | (ns om-css.devcards.sablono 2 | (:require-macros [devcards.core :as dc :refer [defcard deftest]] 3 | [cljs.test :refer [is testing async]]) 4 | (:require [devcards-om-next.core :as don :refer-macros [defcard-om-next]] 5 | [goog.dom :as gdom] 6 | [om.next :as om] 7 | [om.dom :as om-dom] 8 | [om-css.dom :as dom] 9 | [om-css.core :as oc :refer-macros [defui defcomponent]] 10 | [sablono.core :as sab :refer [html]])) 11 | 12 | (defui ^:once SablonoDefui 13 | static oc/Style 14 | (style [_] 15 | [[:.root {:background-color "tomato"}] 16 | [:.section {:background-color :green}]]) 17 | Object 18 | (render [this] 19 | (html 20 | [:div {:class :root} "div with class :root" 21 | [:section {:class :section} "section with class :section"]]))) 22 | 23 | (defcard-om-next sablono-defui 24 | SablonoDefui) 25 | 26 | (defcomponent SablonoDefcomponent [props children] 27 | [[:.root {:background-color "tomato"}] 28 | [:.section {:background-color :green}]] 29 | (html 30 | [:div {:class :root} "div with class :root" 31 | [:section {:class :section} "section with class :section"]])) 32 | 33 | (defcard omcss-12-defcomponent-card 34 | SablonoDefcomponent) 35 | -------------------------------------------------------------------------------- /src/main/om_css/core.cljc: -------------------------------------------------------------------------------- 1 | (ns om-css.core 2 | #?(:cljs (:require-macros [om-css.core :refer [defui defcomponent]] 3 | [om-css.output-css])) 4 | (:require #?@(:clj [[om-css.dom :as dom] 5 | [garden.core :as garden] 6 | [cljs.analyzer.api :as ana]] 7 | :cljs [[om.next :as om]]) 8 | [clojure.string :as string] 9 | [om-css.utils :as utils #?@(:clj [:refer [if-cljs]])]) 10 | #?(:clj (:import (java.io FileNotFoundException)))) 11 | 12 | (defprotocol Style 13 | (style [this])) 14 | 15 | #?(:cljs 16 | (defn prefix-class-name 17 | [x class-name] 18 | "Given a component instance or a component class and a class-name, 19 | prefixes the class-name with the component info" 20 | (let [class (pr-str (cond-> x (om/component? x) type)) 21 | [ns-name component-name] (string/split class #"/") 22 | info {:ns-name ns-name 23 | :component-name component-name}] 24 | (utils/format-class-name class-name info)))) 25 | 26 | #?(:clj 27 | (def css (atom {}))) 28 | 29 | #?(:clj 30 | (defn reshape-props [props component-info classes-seen] 31 | (cond 32 | (map? props) 33 | (let [props' (->> props 34 | (reduce 35 | (fn [m [k v]] 36 | (if (= k :class) 37 | (assoc m k (utils/format-unevaluated-class-names 38 | v component-info classes-seen)) 39 | (assoc m k v))) 40 | {:omcss$info component-info}))] 41 | props') 42 | 43 | (list? props) 44 | (let [[pre post] (split-with (complement map?) props) 45 | props' (concat (map #(cond-> % 46 | (keyword? %) 47 | (utils/format-unevaluated-class-names component-info classes-seen)) 48 | pre) 49 | (map #(reshape-props % component-info classes-seen) post))] 50 | props') 51 | 52 | :else props))) 53 | 54 | #?(:clj 55 | (defn reshape-render 56 | ([form component-info classes-seen] 57 | (reshape-render nil form component-info classes-seen)) 58 | ([env form component-info classes-seen] 59 | (loop [dt (seq form) ret []] 60 | (if dt 61 | (let [form (first dt)] 62 | (if (and (sequential? form) (not (empty? form))) 63 | (let [[[sym props :as pre] post] (split-at 2 form) 64 | sablono? (when (and env (symbol? sym)) 65 | (= (-> (ana/resolve env sym) :name) 66 | 'sablono.core/html)) 67 | coll-fn? (some #{(-> (str sym) 68 | (string/split #"-") 69 | first 70 | symbol)} 71 | ;; TODO: does this need to be hardcoded? 72 | ['map 'keep 'run! 'reduce 'filter 'mapcat]) 73 | props' (if (and coll-fn? (sequential? props)) 74 | (reshape-render env props component-info classes-seen) 75 | (reshape-props props component-info classes-seen)) 76 | props-omitted? (and (sequential? props) 77 | (let [tag (first props)] 78 | (and (or sablono? (symbol? tag) (keyword? tag)) 79 | (some #{(symbol (name tag))} dom/all-tags)))) 80 | pre' (if (and (= (count pre) 2) 81 | (not props-omitted?)) 82 | (list sym props') 83 | (list sym)) 84 | post (cond->> post 85 | props-omitted? (cons props'))] 86 | (recur (next dt) 87 | (into ret 88 | [(cond->> (concat pre' 89 | (reshape-render env post component-info classes-seen)) 90 | (vector? form) (into []))]))) 91 | (recur (next dt) (into ret [form])))) 92 | (seq ret)))))) 93 | 94 | #?(:clj 95 | (defn reshape-defui [env forms component-info classes-seen] 96 | (letfn [(split-on-object [forms] 97 | (split-with (complement '#{Object}) forms)) 98 | (split-on-render [forms] 99 | (split-with 100 | (complement #('#{render} (first %))) 101 | forms))] 102 | (when (seq forms) 103 | (let [[pre [sym & obj-forms :as post]] (split-on-object forms) 104 | ret (into [] pre)] 105 | (if (seq post) 106 | (let [[pre [render & post]] (split-on-render obj-forms)] 107 | (into (conj ret sym) 108 | (concat pre [(reshape-render env render component-info classes-seen)] 109 | post))) 110 | ret)))))) 111 | 112 | #?(:clj 113 | (defn get-style-form [forms] 114 | (loop [dt forms] 115 | (when (seq dt) 116 | (let [form (first dt)] 117 | (if (and (not (sequential? form)) 118 | (not (nil? form)) 119 | (= (name form) "Style")) 120 | (fnext dt) 121 | (recur (rest dt)))))))) 122 | 123 | #?(:clj 124 | (defn reshape-style-form [form] 125 | (drop 2 form))) 126 | 127 | #?(:clj 128 | (defn get-component-style [forms] 129 | (-> forms 130 | get-style-form 131 | reshape-style-form 132 | first))) 133 | 134 | #?(:clj 135 | (defn- munge-ns-name [ns-name] 136 | (string/replace (munge ns-name) #"\." "_"))) 137 | 138 | #?(:clj 139 | (defn- format-garden-class-name [ns-name component-name cns] 140 | "generate namespace qualified classname" 141 | (reduce 142 | #(str %1 "." (munge-ns-name ns-name) 143 | "_" component-name "_" %2) 144 | "" cns))) 145 | 146 | #?(:clj 147 | (defn format-style-classes 148 | [styles ns-name component-name] 149 | (let [classes-seen (atom #{})] 150 | (letfn [(format-style-classes* [styles ns-name component-name] 151 | (->> styles 152 | (clojure.core/map 153 | #(cond 154 | (sequential? %) 155 | (format-style-classes* % ns-name component-name) 156 | 157 | (and (or (keyword? %) (string? %)) 158 | (.contains (name %) ".")) 159 | (let [cn (name %) 160 | cns (remove empty? (string/split cn #"\.")) 161 | elem (when-not (.startsWith cn ".") (first cns))] 162 | (swap! classes-seen into 163 | (map keyword (cond-> cns 164 | (not (nil? elem)) rest))) 165 | (str elem 166 | (format-garden-class-name ns-name component-name 167 | (if elem 168 | (rest cns) 169 | cns)))) 170 | 171 | (and (keyword? %) (.startsWith (name %) "$")) 172 | (str "." (subs (name %) 1)) 173 | 174 | :else %)) 175 | (into [])))] 176 | (let [styles (format-style-classes* styles ns-name component-name)] 177 | {:style styles 178 | :classes @classes-seen}))))) 179 | 180 | #?(:clj 181 | (defn infer-requires [{env-ns :ns :as env} forms] 182 | (letfn [(split-on-symbol [form] 183 | (split-with (complement symbol?) form))] 184 | (loop [dt (seq forms) ret []] 185 | (if dt 186 | (let [form (first dt)] 187 | (cond 188 | (sequential? form) 189 | (recur (next dt) (into ret (infer-requires env form))) 190 | 191 | (symbol? form) 192 | (let [ns (some-> (namespace form) symbol) 193 | req (some->> ns 194 | (get (:requires env-ns)))] 195 | (if req 196 | ;; look in requires 197 | (recur (next dt) 198 | (conj ret `(~'require '[~req :as ~ns]))) 199 | (if-not (nil? (re-find #"clojure.core/" (str (resolve form)))) 200 | ;; clojure function / var 201 | (recur (next dt) ret) 202 | (do 203 | (let [sym-ns (some-> env-ns :defs form :name namespace symbol) 204 | sym-ns (when sym-ns 205 | `(~'use '~sym-ns)) 206 | use-ns (when-let [kv (find (:uses env-ns) form)] 207 | `(~'use '[~(second kv) :only [~(first kv)]]))] 208 | (if (or (= sym-ns (-> env-ns :name)) use-ns) 209 | (recur (next dt) 210 | (conj ret (when sym-ns sym-ns) (when use-ns use-ns))) 211 | (recur (next dt) ret))))))) 212 | :else (recur (next dt) ret))) 213 | ret))))) 214 | 215 | #?(:clj 216 | (defn eval-component-style [style env] 217 | (let [ns-name (-> env :ns :name str) 218 | requires (cons '(clojure.core/refer 'clojure.core) 219 | (infer-requires env style))] 220 | (try 221 | (some->> style 222 | list 223 | (concat requires) 224 | (cons 'do) 225 | eval) 226 | (catch FileNotFoundException e 227 | (throw (IllegalArgumentException. 228 | "Constants must be in a .cljc file."))))))) 229 | 230 | #?(:clj 231 | (defn- get-ns-name [env] 232 | (if-let [ns (:ns env)] 233 | (str (:name ns)) 234 | (str (ns-name *ns*))))) 235 | 236 | #?(:clj 237 | (defn defui* [name forms env] 238 | (let [ns-name (get-ns-name env) 239 | component-name (str name) 240 | component-style (-> forms 241 | get-component-style 242 | (eval-component-style env)) 243 | {:keys [style classes]} (when component-style 244 | (format-style-classes component-style 245 | ns-name component-name)) 246 | css-str (when style 247 | (garden/css style)) 248 | component-info {:ns-name ns-name 249 | :component-name (str name) 250 | :classes classes} 251 | forms (reshape-defui env forms component-info classes) 252 | name (cond-> name 253 | (-> name meta :once) (vary-meta assoc :once true))] 254 | (when css-str 255 | (swap! css assoc [ns-name name] css-str)) 256 | `(if-cljs 257 | (om.next/defui ~name ~@forms) 258 | (cellophane.next/defui ~name ~@forms))))) 259 | 260 | #?(:clj 261 | (defmacro defui [name & forms] 262 | (defui* name forms &env))) 263 | 264 | #?(:clj 265 | (defn defcomponent* 266 | [env name [props children :as args] component-style body] 267 | "Example usage: 268 | (defcomponent foo 269 | [props children] 270 | ;; optional styles vector 271 | [[:.foo {:color :green}] 272 | (dom/div {:class :foo} 273 | children)) 274 | (foo (dom/a {:href \"http://google.com\"})) 275 | " 276 | (when-not (and (vector? args) (= (count args) 2) 277 | ;; arguments are vectors or destructuring maps 278 | (or (symbol? (first args)) (map? (first args))) 279 | (or (symbol? (second args)) (map? (second args)))) 280 | (throw (IllegalArgumentException. 281 | (str "Malformed `defcomponent`. Correct syntax: " 282 | "`(defcomponent [props children] " 283 | "[:.optional {:styles :vector}]" 284 | "(dom/element {:some :props} :children))`")))) 285 | (let [ns-name (get-ns-name env) 286 | component-name (str name) 287 | component-style' (some-> component-style 288 | (eval-component-style env)) 289 | {:keys [style classes]} (when component-style' 290 | (format-style-classes component-style' 291 | ns-name component-name)) 292 | css-str (some-> style 293 | garden/css) 294 | component-info {:ns-name ns-name 295 | :component-name component-name 296 | :classes classes} 297 | body (if (vector? (first body)) 298 | (map #(into [] (reshape-render env % component-info #{})) body) 299 | (reshape-render env body component-info classes))] 300 | (when css-str 301 | (swap! css assoc [ns-name name] css-str)) 302 | `(defn ~name [& params#] 303 | (let [[props# children#] (om-css.dom/parse-params params#) 304 | ~props (assoc props# :omcss$info ~component-info) 305 | ~children children#] 306 | ~@body))))) 307 | 308 | #?(:clj 309 | (defmacro defcomponent 310 | [name props&children & [style & rest :as body]] 311 | (defcomponent* &env name props&children 312 | (when (vector? style) 313 | style) 314 | (if (vector? style) 315 | rest 316 | body)))) 317 | -------------------------------------------------------------------------------- /src/main/om_css/dom.cljc: -------------------------------------------------------------------------------- 1 | (ns om-css.dom 2 | (:refer-clojure :exclude [map meta time mask use]) 3 | #?(:cljs (:require-macros [om-css.dom :refer [gen-tag-fns]])) 4 | (:require #?(:clj [cellophane.dom :as dom] 5 | :cljs [om.dom :as dom]) 6 | [clojure.string :as string] 7 | [om-css.utils :as utils #?@(:clj [:refer [if-cljs]])])) 8 | 9 | ;;; generate all form tags 10 | 11 | #?(:clj 12 | (def form-tags '[input textarea option select])) 13 | 14 | #?(:clj 15 | (def all-tags 16 | ;; cellophane has these tags 17 | (cond-> dom/tags 18 | (not (some (set form-tags) dom/tags)) (concat form-tags)))) 19 | 20 | #?(:clj 21 | (defmacro gen-tag-fns 22 | [] 23 | `(do 24 | ~@(clojure.core/map 25 | (fn [tag] 26 | `(defn ~tag [& ~'params] 27 | (if-cljs 28 | (apply render-element ~(symbol "om.dom" (name tag)) ~'params) 29 | (apply render-element ~(symbol "cellophane.dom" (name tag)) ~'params)))) 30 | all-tags)))) 31 | 32 | (defn camel-case 33 | "Converts kebab-case to camelCase" 34 | [s] 35 | (string/replace s #"-(\w)" (comp string/upper-case second))) 36 | 37 | (defn- opt-key-case 38 | "Converts attributes that are kebab-case and should be camelCase" 39 | [attr] 40 | (if (or (< (count attr) 5) 41 | (case (subs attr 0 5) ("data-" "aria-") true false)) 42 | attr 43 | (camel-case attr))) 44 | 45 | (defn- opt-key-alias 46 | "Converts aliased attributes" 47 | [opt] 48 | (case opt 49 | :class :className 50 | :for :htmlFor 51 | opt)) 52 | 53 | (defn format-opt-key 54 | "Returns potentially formatted name for DOM element attribute. 55 | Converts kebab-case to camelCase." 56 | [opt-key] 57 | (-> opt-key 58 | opt-key-alias 59 | name 60 | opt-key-case 61 | keyword)) 62 | 63 | (declare format-opts) 64 | 65 | (defn format-opt-val 66 | "Returns potentially modified value for DOM element attribute. 67 | Recursively formats map values (ie :style attribute)" 68 | [opt-val] 69 | (cond 70 | (map? opt-val) (format-opts opt-val) 71 | :else opt-val)) 72 | 73 | (defn- format-attrs [attrs] 74 | "Leaves :className unchanged, formats :class accordingly. Converts :ref to string." 75 | (let [map #?(:clj clojure.core/map 76 | :cljs cljs.core/map)] 77 | (->> attrs 78 | (map 79 | (fn [[k v]] 80 | [(format-opt-key k) 81 | (condp = k 82 | :class 83 | (let [component-info (:omcss$info attrs) 84 | classes-seen (:classes component-info)] 85 | (utils/format-dom-class-names v component-info classes-seen)) 86 | 87 | :ref 88 | (str v) 89 | 90 | (format-opt-val v))])) 91 | (reduce (fn [m [k v]] 92 | (if (= k :className) 93 | ;; :omcss$info might end up in classes because we're naively 94 | ;; adding it to a map that appears in props. A stronger 95 | ;; solution might be to check if such map contains the :class keyword 96 | ;; but this might introduce other edge cases. Circle back. 97 | (assoc m k 98 | (string/trim 99 | (str (m k "") 100 | (str " " 101 | (some-> v 102 | (string/replace #":omcss\$info" "")))))) 103 | (cond-> m 104 | (not= k :omcss$info) (assoc k v)))) {})))) 105 | 106 | (defn format-opts 107 | "Returns JavaScript object for React DOM attributes from opts map" 108 | [opts] 109 | (if (map? opts) 110 | (->> opts 111 | format-attrs 112 | #?(:cljs clj->js 113 | :clj (into {} (clojure.core/map 114 | (fn [[k v]] 115 | [k (cond-> v (keyword? v) name)]))))) 116 | opts)) 117 | 118 | (defn parse-params 119 | [params] 120 | (let [props (first params)] 121 | (update 122 | (if (or (nil? props) 123 | #?(:clj (and (map? props) 124 | (not (record? props))) 125 | :cljs (or (and (cljs.core/object? props) 126 | (not (aget props "$$typeof")) 127 | (not= (goog/typeOf (aget props "$$typeof")) "symbol")) 128 | (map? props)))) 129 | [props (rest params)] 130 | [nil params]) 131 | 1 flatten))) 132 | 133 | (defn render-element 134 | [render & params] 135 | (let [[attrs children] (parse-params params)] 136 | (apply render (format-opts attrs) children))) 137 | 138 | (gen-tag-fns) 139 | 140 | ;;; proxy thru to om.dom 141 | #?(:cljs 142 | (defn render 143 | [& params] 144 | (apply dom/render params))) 145 | 146 | (defn render-to-str 147 | [& params] 148 | (apply dom/render-to-str params)) 149 | 150 | (defn node 151 | [& params] 152 | (apply dom/node params)) 153 | -------------------------------------------------------------------------------- /src/main/om_css/output_css.clj: -------------------------------------------------------------------------------- 1 | (ns om-css.output-css 2 | (:require [cljs.analyzer.api :as ana-api] 3 | [clojure.java.io :as io] 4 | [clojure.string :as string])) 5 | 6 | (defn setup-io! [] 7 | (let [{:keys [css-output-to output-dir output-to]} (ana-api/get-options) 8 | default-fname "out.css" 9 | fname (or css-output-to 10 | (str output-dir default-fname) 11 | (string/join "/" 12 | (-> output-to 13 | (string/split #"/") 14 | pop 15 | (conj default-fname))))] 16 | (add-watch om-css.core/css :watcher 17 | (fn [k atom old-state new-state] 18 | (with-open [out ^java.io.Writer (io/make-writer fname {})] 19 | (binding [*out* out] 20 | (println (string/join "\n" (vals new-state))) 21 | (println))))))) 22 | 23 | (setup-io!) 24 | -------------------------------------------------------------------------------- /src/main/om_css/utils.cljc: -------------------------------------------------------------------------------- 1 | (ns om-css.utils 2 | (:require [clojure.string :as string])) 3 | 4 | #?(:clj 5 | (defn- cljs-env? [env] 6 | (boolean (:ns env)))) 7 | 8 | #?(:clj 9 | (defmacro if-cljs 10 | "Return `then` if we are generating cljs code and `else` for Clojure code." 11 | [then else] 12 | (if (cljs-env? &env) then else))) 13 | 14 | 15 | (defn format-class-name [class-name component-info] 16 | "generate namespace qualified classname" 17 | (if (symbol? class-name) 18 | class-name 19 | (let [ns-name (:ns-name component-info) 20 | class-name (name class-name) 21 | component-name (-> (:component-name component-info) 22 | (string/split #"/") 23 | last)] 24 | (str (string/replace (munge ns-name) #"\." "_") 25 | "_" component-name "_" class-name)))) 26 | 27 | 28 | (defn format-unevaluated-class-names 29 | ([cns component-info] 30 | (format-unevaluated-class-names cns component-info true)) 31 | ([cns component-info classes-seen] 32 | ;; unevaluated data structures: a list might be a function call, we 33 | ;; only support strings, vectors or keywords 34 | (cond 35 | (or (vector? cns) 36 | (string? cns) 37 | (keyword? cns)) 38 | (let [cns' (map #(cond-> % 39 | (and classes-seen 40 | (or (true? classes-seen) 41 | (get classes-seen %))) 42 | (format-class-name component-info)) 43 | (if (sequential? cns) cns [cns]))] 44 | (if (sequential? cns) 45 | (into [] cns') 46 | (first cns'))) 47 | 48 | (map? cns) 49 | (into {} (map (fn [[k v]] 50 | [(format-unevaluated-class-names k component-info classes-seen) 51 | (format-unevaluated-class-names v component-info classes-seen)])) cns) 52 | 53 | (list? cns) 54 | (map #(format-unevaluated-class-names % component-info classes-seen) cns) 55 | 56 | :else cns))) 57 | 58 | ;; only transform keywords at runtime, vectors and strings have 59 | ;; already been prefixed at macro-expansion time 60 | (defn format-dom-class-names [cns component-info classes-seen] 61 | (->> (if (sequential? cns) cns [cns]) 62 | (map #(cond-> % 63 | (keyword? %) name 64 | (get classes-seen %) (format-class-name component-info))) 65 | (string/join " "))) 66 | -------------------------------------------------------------------------------- /src/test/om_css/runner.cljs: -------------------------------------------------------------------------------- 1 | (ns om-css.runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | [om-css.tests])) 4 | 5 | (doo-tests 'om-css.tests) 6 | -------------------------------------------------------------------------------- /src/test/om_css/tests.clj: -------------------------------------------------------------------------------- 1 | (ns om-css.tests 2 | (:require [clojure.test :refer [deftest testing is are]] 3 | [om-css.core :as oc :refer [defui defcomponent]] 4 | [om-css.dom :as dom] 5 | [cellophane.next :as cellophane] 6 | [cellophane.dom :as cdom] 7 | [om-css.utils :as utils] 8 | [cljs.analyzer.api :as ana])) 9 | 10 | (def component-info 11 | {:component-name "Foo" 12 | :ns-name "ns.core"}) 13 | 14 | (deftest test-reshape-render 15 | (testing "`reshape-render` adds ns & component info to props" 16 | (let [unchanged '((dom/div nil "text"))] 17 | (is (= (oc/reshape-render 18 | '((dom/div {} 19 | "Nested `defcomponent` example")) 20 | component-info nil) 21 | '((dom/div {:omcss$info {:component-name "Foo" 22 | :ns-name "ns.core"}} 23 | "Nested `defcomponent` example")))) 24 | (is (= (oc/reshape-render unchanged component-info nil) unchanged)))) 25 | (testing "`reshape-render` adds namespace qualified classes (:class)" 26 | (is (= (oc/reshape-render 27 | '((dom/div {:class :bar} "bar")) 28 | component-info #{:bar}) 29 | '((dom/div {:omcss$info {:component-name "Foo" 30 | :ns-name "ns.core"} 31 | :class "ns_core_Foo_bar"} "bar")))) 32 | (is (= (oc/reshape-render 33 | '((dom/div {:class :bar} "bar" 34 | (dom/p {:class :baz} "baz"))) 35 | component-info #{:bar :baz}) 36 | '((dom/div {:omcss$info {:component-name "Foo" 37 | :ns-name "ns.core"} 38 | :class "ns_core_Foo_bar"} "bar" 39 | (dom/p {:omcss$info {:component-name "Foo" 40 | :ns-name "ns.core"} 41 | :class "ns_core_Foo_baz"} "baz")))))) 42 | (testing "`reshape-render` preserves `:className` classnames" 43 | (is (= (oc/reshape-render 44 | '((dom/div {:className "bar"} "bar")) 45 | component-info nil) 46 | '((dom/div {:omcss$info {:component-name "Foo" 47 | :ns-name "ns.core"} 48 | :className "bar"} "bar"))))) 49 | (testing "`reshape-render` preserves `:class`'s data structure" 50 | (is (= (oc/reshape-render 51 | '((dom/div {:class [:root]})) 52 | component-info #{:root}) 53 | '((dom/div {:omcss$info {:component-name "Foo" 54 | :ns-name "ns.core"} 55 | :class ["ns_core_Foo_root"]}))))) 56 | (testing "`reshape-render` skips `let` bindings" 57 | (let [form '((let [x true] 58 | (dom/div 59 | {:class [:root :active]} 60 | "div with class root" 61 | (dom/hr) 62 | (dom/section {:class :section} 63 | "section with class :section" 64 | children))))] 65 | (is (= (oc/reshape-render form component-info #{:root :active :section}) 66 | '((let [x true] 67 | (dom/div 68 | {:class ["ns_core_Foo_root" "ns_core_Foo_active"] 69 | :omcss$info {:component-name "Foo" 70 | :ns-name "ns.core"}} 71 | "div with class root" 72 | (dom/hr) 73 | (dom/section {:class "ns_core_Foo_section" 74 | :omcss$info {:component-name "Foo" 75 | :ns-name "ns.core"}} 76 | "section with class :section" 77 | children))))))))) 78 | 79 | (deftest test-get-style 80 | (let [form '(static om/IQuery 81 | (query [this]) 82 | static oc/Style 83 | (style [_] 84 | [:root {:color "#FFFFF"} 85 | :section {:background-color :green}]) 86 | static om/Ident 87 | (ident [this]) 88 | Object 89 | (render [this]) 90 | static om/IQueryParams 91 | (params [this]))] 92 | (is (= (oc/get-style-form form) 93 | '(style [_] 94 | [:root {:color "#FFFFF"} 95 | :section {:background-color :green}]))) 96 | (is (nil? (oc/get-style-form 97 | '(Object 98 | (render [this]) 99 | static om/Ident 100 | (ident [this]))))) 101 | (is (= (oc/get-component-style form) 102 | [:root {:color "#FFFFF"} 103 | :section {:background-color :green}])))) 104 | 105 | (deftest test-reshape-defui 106 | (let [form '(om/IQuery 107 | (query [this]) 108 | om/Ident 109 | (ident [this]) 110 | Object 111 | (componentWillMount [this]) 112 | (render [dia] 113 | (dom/div {:class :foo} (dom/div nil "3"))) 114 | static field a 3 115 | static om/IQuery 116 | (query [this] [:a])) 117 | expected '[om/IQuery 118 | (query [this]) 119 | om/Ident 120 | (ident [this]) 121 | Object 122 | (componentWillMount [this]) 123 | (render [dia] 124 | (dom/div 125 | {:class "ns_core_Foo_foo" 126 | :omcss$info {:component-name "Foo" 127 | :ns-name "ns.core"}} 128 | (dom/div nil "3"))) 129 | static field a 3 130 | static om/IQuery 131 | (query [this] [:a])]] 132 | (is (= (oc/reshape-defui nil form component-info #{:foo}) 133 | expected)) 134 | (is (= (oc/reshape-defui nil 135 | '(Object (render [this] (dom/div nil "foo"))) 136 | component-info nil) 137 | '[Object (render [this] (dom/div nil "foo"))])))) 138 | 139 | (deftest test-infer-requires 140 | (let [env '{:ns {:name ns.core 141 | :requires {c ns.constants 142 | o ns.other}}}] 143 | (are [forms res] (= (oc/infer-requires env forms)) 144 | '[[:.root {:background-color "tomato"}] 145 | [:.section (merge c/style-1 {:background-color :green})]] 146 | '[(require '[ns.constants :as c])] 147 | 148 | '[[:.root (merge o/style-2 {:background-color "tomato"})] 149 | [:.section (merge c/style-1 {:background-color :green})]] 150 | '[(require '[ns.other :as o]) 151 | (require '[ns.constants :as c])] 152 | 153 | '[:$desktop 154 | [:.root (merge c/style-1 {:background-color "tomato"})]] 155 | '[(require '[ns.constants :as c])]))) 156 | 157 | (deftest omcss-11 158 | (let [form1 '((dom/div (merge props {:class :root}) 159 | "purple")) 160 | form2 '((dom/div (merge {:class :root} props) "purple"))] 161 | (is (= (oc/reshape-render form1 component-info #{:root}) 162 | '((dom/div (merge props {:omcss$info {:component-name "Foo" 163 | :ns-name "ns.core"} 164 | :class "ns_core_Foo_root"}) 165 | "purple")))) 166 | (is (= (oc/reshape-render form2 component-info #{:root}) 167 | '((dom/div (merge {:omcss$info {:component-name "Foo" 168 | :ns-name "ns.core"} 169 | :class "ns_core_Foo_root"} 170 | props) 171 | "purple")))))) 172 | 173 | (deftest test-reshape-props 174 | (are [props classes res] (= (oc/reshape-props props component-info classes) res) 175 | '(merge {:class "foo"}) #{"foo"} '(merge {:omcss$info {:ns-name "ns.core" 176 | :component-name "Foo"} 177 | :class "ns_core_Foo_foo"}) 178 | {:class "foo"} #{"foo"} {:omcss$info {:ns-name "ns.core" 179 | :component-name "Foo"} 180 | :class "ns_core_Foo_foo"} 181 | {:class :foo} #{:foo} {:omcss$info {:ns-name "ns.core" 182 | :component-name "Foo"} 183 | :class "ns_core_Foo_foo"} 184 | ;; TODO: is this intended behavior? 185 | ;; see OMCSS-17 186 | '(merge {:class (subs (str :foo) 1)}) #{:foo} '(merge 187 | {:omcss$info {:component-name "Foo" 188 | :ns-name "ns.core"}, 189 | :class (subs (str "ns_core_Foo_foo") 1)}))) 190 | 191 | (deftest test-format-class-names 192 | (are [cns res] (= (utils/format-unevaluated-class-names cns component-info) res) 193 | :foo "ns_core_Foo_foo" 194 | "foo" "ns_core_Foo_foo" 195 | [:foo] ["ns_core_Foo_foo"] 196 | ["foo"] ["ns_core_Foo_foo"] 197 | ["foo" :bar] ["ns_core_Foo_foo" "ns_core_Foo_bar"] 198 | '(keys {:root true}) '(keys {"ns_core_Foo_root" true}))) 199 | 200 | (deftest test-omcss-15 201 | (let [form '((let [color :red size :xl] 202 | (dom/div {:class [color size]})))] 203 | (is (= (oc/reshape-render form component-info nil) 204 | '((let [color :red size :xl] 205 | (dom/div {:class [color size] 206 | :omcss$info {:component-name "Foo" 207 | :ns-name "ns.core"}}))))))) 208 | 209 | (deftest test-omcss-17 210 | (let [form '((dom/div nil 211 | (inner {:class (flatten [:outer class])} 212 | children)))] 213 | (is (= (oc/reshape-render form 214 | {:ns-name "om-css.devcards.bugs" 215 | :component-name "outer"} 216 | #{:outer}) 217 | '((dom/div nil 218 | (inner {:omcss$info {:ns-name "om-css.devcards.bugs" 219 | :component-name "outer"} 220 | :class (flatten ["om_css_devcards_bugs_outer_outer" class])} 221 | children))))))) 222 | 223 | (deftest test-omcss-20 224 | (let [form '((let [dir "even"] 225 | (dom/div 226 | {:class (if (= dir "even") [:even] [])})))] 227 | (is (= (oc/reshape-render form component-info #{:even}) 228 | '((let [dir "even"] 229 | (dom/div {:omcss$info {:component-name "Foo" 230 | :ns-name "ns.core"} 231 | :class (if (= dir "even") 232 | ["ns_core_Foo_even"] 233 | [])}))))))) 234 | 235 | (deftest test-format-style-classes 236 | (let [{:keys [ns-name component-name]} component-info] 237 | (testing "" 238 | (are [style res] (= (oc/format-style-classes style ns-name component-name) 239 | res) 240 | [:.root {:color :purple}] {:style [".ns_core_Foo_root" 241 | {:color :purple}] 242 | :classes #{:root}} 243 | [[:.root {:color :purple}] 244 | [:.section {:text-align :center}]] {:style [[".ns_core_Foo_root" {:color :purple}] 245 | [".ns_core_Foo_section" {:text-align :center}]] 246 | :classes #{:root :section}})) 247 | (testing "OMCSS-19" 248 | (is (= (#'oc/format-garden-class-name ns-name component-name ["root"]) 249 | ".ns_core_Foo_root")) 250 | (is (= (oc/format-style-classes 251 | [:h1.root {:color "#FFFFF"}] 252 | ns-name component-name) 253 | {:style ["h1.ns_core_Foo_root" {:color "#FFFFF"}] 254 | :classes #{:root}}))))) 255 | 256 | (deftest test-omcss-23 257 | (let [form '((outer 258 | {:class :outer} 259 | (inner {:class :inner} "inner")))] 260 | (is (= (oc/reshape-render form 261 | {:ns-name "om-css.devcards.bugs" 262 | :component-name "wrapper"} 263 | #{:outer :inner}) 264 | '((outer {:omcss$info {:ns-name "om-css.devcards.bugs" 265 | :component-name "wrapper"} 266 | :class "om_css_devcards_bugs_wrapper_outer"} 267 | (inner {:omcss$info {:ns-name "om-css.devcards.bugs" 268 | :component-name "wrapper"} 269 | :class "om_css_devcards_bugs_wrapper_inner"} "inner"))))))) 270 | 271 | (deftest test-omcss-24 272 | (let [form '(((if true dom/div dom/span) children))] 273 | (is (= (oc/reshape-render form component-info #{}) 274 | form)))) 275 | 276 | (deftest test-omcss-27 277 | (let [form '((dom/div (my-class :root)))] 278 | (is (= (oc/reshape-render form component-info #{:root}) 279 | '((dom/div (my-class "ns_core_Foo_root"))))))) 280 | 281 | (deftest test-nested-fns-inside-element 282 | (let [form '((dom/div nil 283 | "something" 284 | (map-indexed 285 | (fn [index _] 286 | (dom/p {:class :hi} (str "index: " index))) 287 | [1 2 3 4])))] 288 | (is (= (oc/reshape-render form component-info #{:hi}) 289 | '((dom/div nil 290 | "something" 291 | (map-indexed 292 | (fn [index _] 293 | (dom/p {:class "ns_core_Foo_hi" 294 | :omcss$info {:component-name "Foo" 295 | :ns-name "ns.core"}} 296 | (str "index: " index))) 297 | [1 2 3 4])))))) 298 | (let [form '((dom/div nil 299 | (->> [1 2] (map my-fn))))] 300 | (is (= (oc/reshape-render form component-info #{}) 301 | form)))) 302 | 303 | (defui SimpleDefui 304 | oc/Style 305 | (style [_] 306 | [:.root {:color :green}]) 307 | Object 308 | (render [this] 309 | (dom/div {:id "simple" 310 | :class :root} 311 | "root div"))) 312 | 313 | (defcomponent SimpleDefcomponent [props children] 314 | [:.inline {:display "inline"}] 315 | (dom/div {:class :inline} "inline div")) 316 | 317 | (deftest test-om-css-cellophane 318 | (testing "cellophane & defui" 319 | (let [c ((cellophane/factory SimpleDefui))] 320 | (is (= (str (#'cdom/render-to-str* c)) 321 | "
root div
")))) 322 | (testing "cellophane & defcomponent" 323 | (is (= (str (#'cdom/render-to-str* (SimpleDefcomponent))) 324 | "
inline div
")))) 325 | 326 | (defui LazySeqChild 327 | Object 328 | (render [this] 329 | (let [props (cellophane/props this)] 330 | (dom/div {:class (:class props)} "bar")))) 331 | 332 | (def lazy-seq-child (cellophane/factory LazySeqChild)) 333 | 334 | (defui LazySeqParent 335 | oc/Style 336 | (style [_] 337 | [[:.foo {:text-align "center"}]]) 338 | Object 339 | (render [this] 340 | (dom/div nil 341 | (lazy-seq-child (assoc {} :class 342 | (filter some? ["foo" nil])))))) 343 | 344 | (deftest test-om-css-cellophane-lazy-seqs 345 | (is (= (str (#'cdom/render-to-str* ((cellophane/factory LazySeqParent)))) 346 | "
bar
"))) 347 | 348 | (deftest test-om-css-30 349 | (is (= (oc/reshape-render '((dom/div (dom/div (dom/div {:class :component})))) 350 | component-info #{:component}) 351 | '((dom/div (dom/div (dom/div {:omcss$info {:component-name "Foo", :ns-name "ns.core"} 352 | :class "ns_core_Foo_component"}))))))) 353 | 354 | (deftest test-format-opts 355 | (are [opts res] (= (dom/format-opts opts) res) 356 | {:media :desktop} {:media "desktop"} 357 | {:class :foo 358 | :omcss$info (merge component-info 359 | {:classes #{:foo}})} {:className "ns_core_Foo_foo"})) 360 | 361 | (deftest test-sablono-integration 362 | (with-redefs [ana/resolve (fn [_ sym] 363 | (when (= (name sym) "html") 364 | {:arglists '([content]), 365 | :doc "Compile the Hiccup `content` into a React DOM node.", 366 | :line 10, :column 1, 367 | :file "sablono/core.clj", 368 | :name 'sablono.core/html, 369 | :ns 'sablono.core, 370 | :macro true}))] 371 | (is (= (oc/reshape-render {} '((html [:div {:class :component}])) 372 | component-info #{:component}) 373 | '((html [:div {:omcss$info {:component-name "Foo", :ns-name "ns.core"} 374 | :class "ns_core_Foo_component"}])))) 375 | (is (= (oc/reshape-render {} '((sab/html [:div [:div {:class :component}]])) 376 | component-info #{:component}) 377 | '((sab/html [:div [:div {:omcss$info {:component-name "Foo", :ns-name "ns.core"} 378 | :class "ns_core_Foo_component"}]])))))) 379 | -------------------------------------------------------------------------------- /src/test/om_css/tests.cljs: -------------------------------------------------------------------------------- 1 | (ns om-css.tests 2 | (:require [cljs.test :refer-macros [deftest testing is are run-tests]] 3 | [cljsjs.react] 4 | [om-css.core :as oc :refer-macros [defui]] 5 | [om-css.utils :as utils])) 6 | 7 | (def component-info 8 | {:component-name "Foo" 9 | :ns-name "ns.core"}) 10 | 11 | (deftest test-format-class-name 12 | (are [cn res] (= (utils/format-class-name cn component-info) res) 13 | :foo "ns_core_Foo_foo" 14 | "foo" "ns_core_Foo_foo") 15 | (is (= (utils/format-class-name 16 | :foo 17 | {:ns-name "ns.core" 18 | :component-name "ns.core/Foo"}) 19 | "ns_core_Foo_foo"))) 20 | 21 | (deftest test-format-dom-class-names 22 | (are [cns classes-seen res] (= (utils/format-dom-class-names cns component-info classes-seen) res) 23 | "ns_core_Foo_foo" #{} "ns_core_Foo_foo" 24 | :foo #{:foo} "ns_core_Foo_foo" 25 | [:foo] #{:foo} "ns_core_Foo_foo" 26 | [:foo :bar] #{:foo :bar} "ns_core_Foo_foo ns_core_Foo_bar" 27 | ["ns_core_Foo_foo"] nil "ns_core_Foo_foo" 28 | ["ns_core_Foo_foo"] nil "ns_core_Foo_foo" 29 | ["ns_core_Foo_foo" "ns_core_Foo_bar"] nil "ns_core_Foo_foo ns_core_Foo_bar" 30 | ["ns_core_Foo_foo" :bar] #{:bar} "ns_core_Foo_foo ns_core_Foo_bar")) 31 | 32 | (defui StyledComponent 33 | static oc/Style 34 | (style [this] 35 | [[:.some-class {:text-align "center"}]])) 36 | 37 | (deftest test-prefix-class-name 38 | (is (= (oc/prefix-class-name StyledComponent :some-class) 39 | "om_css_tests_StyledComponent_some-class"))) 40 | --------------------------------------------------------------------------------