├── .gitignore ├── LICENSE ├── README.md ├── gulpfile.js ├── package.json ├── project.clj ├── resources └── public │ ├── css │ ├── foreign-lib.js │ └── style.css │ ├── img │ └── logo.png │ └── index.html └── src └── clojurescript_css_modules_demo ├── css ├── body.css ├── common.css ├── header.css └── settings.js ├── js ├── core.cljs └── css.cljs └── macros.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /resources/public/js/compiled/** 2 | figwheel_server.log 3 | pom.xml 4 | *jar 5 | /lib/ 6 | /classes/ 7 | /out/ 8 | /target/ 9 | .lein-deps-sum 10 | .lein-repl-history 11 | .lein-plugins/ 12 | .repl 13 | .nrepl-port 14 | .idea 15 | /resources/public/css 16 | /resources/public/js 17 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dhruv Bhatia 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSS Modules in ClojureScript 2 | 3 | ![](resources/public/img/logo.png) 4 | 5 | A sample ClojureScript project showcasing a workflow with live-reloadable [CSS Modules](http://glenmaddern.com/articles/css-modules) using Gulp & Figwheel. 6 | 7 | ## How It Works 8 | 9 | The basic premise is as follows: 10 | 11 | 1. A Gulp task watches and compiles your CSS Modules to JS. 12 | * *Note: CSS Module namespaces to be exported are defined in `src/clojurescript_css_modules_demo/css/settings.js`.* 13 | 2. This JS file is hooked into your CLJS app through a `foreign-lib` named `cssModules`. 14 | 3. Figwheel reloads your project whenever it detects changes to `cssModules`. 15 | 4. This module is exposed to your CLJS project, allowing you to easily access the class names of your CSS Modules. 16 | * *Note: a helper macro is provided for use with Reagent's hiccup-style syntax, but you can use this flow with any CLJS project.* 17 | 18 | ## Installation 19 | Clone, `npm install` and `gulp watch` in one REPL. 20 | `lein figwheel` in another REPL. 21 | Change your .css modules within `src/css/` and notice how Gulp/Figwheel live reload your project. 22 | 23 | Note: Remember to change `css/settings.js` if you add additional CSS Module namespaces. 24 | 25 | 26 | ## Usage With Reagent 27 | At first - import CSS modules to project with foreign-libs. After it you can use `aget` to lookup and replace classes in Reagent templates with the correct css classes from your files. 28 | 29 | 30 | For example: in `filename.css` we get CSS rule with selector `.logo`: 31 | 32 | ```clojure 33 | (defn my-component[] 34 | [:div { :class-name (aget js/cssModules "filename" "logo") }]) 35 | ``` 36 | 37 | #### Syntax Sugar 38 | 39 | There is a pretty syntax sugar inside demo: 40 | 41 | ```clojure 42 | (defn my-component[] 43 | (get-css 44 | [:div.CSS>filename>logo])) 45 | ``` 46 | 47 | Which is equivalent to code above. Or you can use macro `defcomponent` which wraps your component function with `get-css`. 48 | 49 | ```clojure 50 | (defcomponent my-component[] 51 | [:div.CSS>filename>logo]) 52 | ``` 53 | 54 | ## TODO: 55 | * [ ] Expand on this documentation! 56 | * [ ] Structure CSS Modules, add advanced examples. 57 | * [ ] Explore making this a standalone Clojure plugin (thereby removing the NodeJS/Gulp dependency). 58 | 59 | Contributions welcome! -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var browserify = require('browserify'); 3 | var source = require('vinyl-source-stream'); 4 | 5 | gulp.task('css-modules', function () { 6 | 7 | return browserify({ 8 | debug: true, 9 | standalone: 'cssModules' 10 | }) 11 | 12 | .transform("babelify", {presets: ["es2015"]}) 13 | .require('./src/clojurescript_css_modules_demo/css/settings.js', {entry: true}) 14 | .plugin('css-modulesify', { 15 | o: './resources/public/css/style.css', 16 | rootDir: './src/clojurescript_css_modules_demo/css', 17 | use: [ 18 | 'postcss-import', 19 | 'cssnext', 20 | 'postcss-modules-extract-imports', 21 | 'postcss-modules-local-by-default', 22 | 'postcss-modules-scope', 23 | ], 24 | 'cssnext': { 25 | compress: false, 26 | }, 27 | 'postcss-import': { 28 | 'path': __dirname + '/src/clojurescript_css_modules_demo/css' 29 | } 30 | //jsonOutput: './resources/public/css/app.json', // generating a json file and using that to generate css class names could also be an option 31 | }) 32 | .bundle() 33 | .on("error", function (err) { 34 | console.log("Error : " + err.message); 35 | }) 36 | .pipe(source('foreign-lib.js')) 37 | .pipe(gulp.dest('./resources/public/css')); 38 | 39 | }); 40 | 41 | gulp.task("watch", function () { 42 | // calls "css-modules" whenever anything changes 43 | gulp.watch("src/clojurescript_css_modules_demo/css/*.*", ["css-modules"]); 44 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clojurescript-css-modules-demo", 3 | "version": "1.0.0", 4 | "description": "An example of a reloadable Clojurescript workflow using CSS Modules", 5 | "main": "index.js", 6 | "dependencies": { 7 | }, 8 | "devDependencies": { 9 | "babel-preset-es2015-loose": "^7.0.0", 10 | "babel-preset-es2015": "^6.6.0", 11 | "babelify": "^7.2.0", 12 | "browserify": "^13.0.0", 13 | "css-modulesify": "^0.23.0", 14 | "cssnext": "^1.8.4", 15 | "gulp": "^3.9.1", 16 | "postcss-import": "^8.1.0", 17 | "postcss-modules-extract-imports": "^1.0.0", 18 | "postcss-modules-local-by-default": "^1.0.1", 19 | "postcss-modules-scope": "^1.0.0", 20 | "vinyl-source-stream": "^1.1.0", 21 | "require-all": "^2.0.0" 22 | }, 23 | "scripts": { 24 | "test": "echo \"Error: no test specified\" && exit 1" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/dhruvbhatia/clojurescript-css-modules-demo.git" 29 | }, 30 | "author": "", 31 | "license": "ISC", 32 | "bugs": { 33 | "url": "https://github.com/dhruvbhatia/clojurescript-css-modules-demo/issues" 34 | }, 35 | "homepage": "https://github.com/dhruvbhatia/clojurescript-css-modules-demo#readme" 36 | } 37 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject clojurescript-css-modules-demo "0.1.1-SNAPSHOT" 2 | :description "Simple ClojureScript with CSS modules Demo" 3 | :url "https://github.com/dhruvbhatia/clojurescript-css-modules-demo" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | 7 | :dependencies [[org.clojure/clojure "1.7.0"] 8 | [org.clojure/clojurescript "1.7.122"] 9 | [org.clojure/core.async "0.2.374"] 10 | [reagent "0.5.1"]] 11 | 12 | :plugins [[lein-cljsbuild "1.1.0"] 13 | [lein-figwheel "0.4.1"] 14 | [lein-ancient "0.6.8"]] 15 | 16 | :source-paths ["src"] 17 | 18 | :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] 19 | 20 | :cljsbuild { 21 | :builds [{:id "dev" 22 | :source-paths ["src"] 23 | 24 | :figwheel { :on-jsload "clojurescript-css-modules-demo.js.core/init" } 25 | 26 | :compiler {:main clojurescript-css-modules-demo.js.core 27 | :asset-path "js/compiled/out" 28 | :output-to "resources/public/js/compiled/clojurescript_css_modules_demo.js" 29 | :output-dir "resources/public/js/compiled/out" 30 | :source-map-timestamp true 31 | :foreign-libs [{:file "resources/public/css/foreign-lib.js" 32 | :provides ["cssModules"]}]}} 33 | {:id "min" 34 | :source-paths ["src"] 35 | :compiler {:output-to "resources/public/js/compiled/clojurescript_css_modules_demo.js" 36 | :main clojurescript-css-modules-demo.js.core 37 | :optimizations :advanced 38 | :pretty-print false 39 | :foreign-libs [{:file "resources/public/css/foreign-lib.js" 40 | :provides ["cssModules"]}]}}]} 41 | 42 | :figwheel { 43 | ;; :http-server-root "public" ;; default and assumes "resources" 44 | ;; :server-port 3449 ;; default 45 | ;; :server-ip "127.0.0.1" 46 | 47 | :css-dirs ["resources/public/css"] ;; watch and update CSS 48 | 49 | ;; Start an nREPL server into the running figwheel process 50 | ;; :nrepl-port 7888 51 | 52 | ;; Server Ring Handler (optional) 53 | ;; if you want to embed a ring handler into the figwheel http-kit 54 | ;; server, this is for simple ring servers, if this 55 | ;; doesn't work for you just run your own server :) 56 | ;; :ring-handler hello_world.server/handler 57 | 58 | ;; To be able to open files in your editor from the heads up display 59 | ;; you will need to put a script on your path. 60 | ;; that script will have to take a file path and a line number 61 | ;; ie. in ~/bin/myfile-opener 62 | ;; #! /bin/sh 63 | ;; emacsclient -n +$2 $1 64 | ;; 65 | ;; :open-file-command "myfile-opener" 66 | 67 | ;; if you want to disable the REPL 68 | ;; :repl false 69 | 70 | ;; to configure a different figwheel logfile path 71 | ;; :server-logfile "tmp/logs/figwheel-logfile.log" 72 | }) 73 | -------------------------------------------------------------------------------- /resources/public/css/foreign-lib.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.cssModules = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/clojurescript_css_modules_demo/css/body.css: -------------------------------------------------------------------------------- 1 | @import 'common.css'; 2 | 3 | .container { 4 | width: 768px; 5 | background-color: beige; 6 | margin: 0 auto; 7 | font-family: var(--font); 8 | } 9 | 10 | .article { 11 | color: dark-blue; 12 | } 13 | 14 | .footer { 15 | font-size: 10px; 16 | } -------------------------------------------------------------------------------- /src/clojurescript_css_modules_demo/css/common.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-colour: #BBCFE7; 3 | --secondary-colour: #e7df72; 4 | --font: Helvetica Neue, Helvetica, Arial, sans-serif; 5 | } 6 | 7 | .heading { 8 | font-size: 2em; 9 | } -------------------------------------------------------------------------------- /src/clojurescript_css_modules_demo/css/header.css: -------------------------------------------------------------------------------- 1 | @import 'common.css'; 2 | 3 | :root { 4 | --logo-width: 600px; 5 | --logo-height: 250px; 6 | } 7 | 8 | .container { 9 | background-color: var(--primary-colour); 10 | font-family: var(--font); 11 | display: inline-block; 12 | width: 100%; 13 | } 14 | 15 | .logo { 16 | height: var(--logo-height); 17 | width: var(--logo-width); 18 | background-color: var(--secondary-colour); 19 | background-image: url('/img/logo.png'); 20 | background-repeat: no-repeat; 21 | float: left; 22 | } 23 | 24 | .site-name { 25 | composes: heading from 'common.css'; 26 | background-color: #0b97c4; 27 | } -------------------------------------------------------------------------------- /src/clojurescript_css_modules_demo/css/settings.js: -------------------------------------------------------------------------------- 1 | import header from './header.css'; 2 | import body from './body.css'; 3 | 4 | 5 | export { 6 | header, body 7 | } 8 | 9 | // The below code is an experiment to automagically import and namespace CSS Modules, so users don't need to update this file manually. Needs a bit of work, though. 10 | 11 | //var controllers = require('require-all')({ 12 | // dirname : __dirname, 13 | // recursive : true 14 | //}); 15 | // 16 | //console.log(controllers); 17 | 18 | //var fs = require('fs'); 19 | // 20 | //var modules = []; 21 | // 22 | //fs.readdirSync(__dirname + '/').forEach(function(file) { 23 | // if (file.match(/\.css$/) !== null && file !== 'index.js') { 24 | // var name = file.replace('.css', ''); 25 | // var filepath = './' + file; 26 | // 27 | // modules.push({name: name, path: filepath}); 28 | // 29 | // //import name from filepath; 30 | // 31 | // console.log(exports); 32 | // 33 | // module.exports = {header: fs.readFileSync(__dirname + '/' + file, 'utf8')}; 34 | // 35 | // //console.log(fs.readFileSync(__dirname + '/' + file, 'utf8')); 36 | // //module.exports = fs.readFileSync('./' + file, 'utf8'); 37 | // } 38 | //}); 39 | 40 | 41 | 42 | //modules.forEach(function(i) { 43 | // import i.name from i.path; 44 | //}); 45 | -------------------------------------------------------------------------------- /src/clojurescript_css_modules_demo/js/core.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:figwheel-always clojurescript-css-modules-demo.js.core 2 | (:require [cssModules] 3 | [reagent.core :as r] 4 | [clojurescript-css-modules-demo.js.css :as css :refer [get-css]] 5 | ) 6 | (:require-macros [clojurescript-css-modules-demo.macros :refer [defcomponent]])) 7 | 8 | (enable-console-print!) 9 | 10 | ; print all included css modules to console - anything exported from css/settings.js will be available here 11 | (println "CSS Modules:" js/cssModules) 12 | 13 | (defn header [] 14 | (get-css 15 | ; we can wrap out reagent templates with get-css and use the syntax below to lookup/replace with the correct css classes 16 | [:div.CSS>header>container 17 | ;alternatively, you can use the below and lookup the cssModules object directly 18 | ;{:class-name (aget js/cssModules "header" "container")} 19 | [:p ".container from header.css"] 20 | 21 | [:div.CSS>header>logo 22 | ;{:class-name (aget js/cssModules "header" "logo")} 23 | [:p ".logo from header.css"] 24 | ] 25 | 26 | [:div.CSS>header>site-name 27 | ;{:class-name (aget js/cssModules "header" "site-name")} 28 | [:p ".site-name from header.css"] 29 | [:small "Note how this div automatically pulls in and applies dependant CSS class names using only the 'div.CSS>header>site-name' syntax. Try changing .header in common.css and this will also update."] 30 | ] 31 | 32 | ] 33 | ) 34 | ) 35 | 36 | ;; alternatively, you can use macros from macros namespace which 37 | ;; wraps your component function with get-css function 38 | (defcomponent body [] 39 | [:div.CSS>body>container 40 | [:h1 "H1 in div .container from body.css"] 41 | 42 | [:article.CSS>body>article 43 | [:p "p in article with .article from body.css"]] 44 | 45 | [:div.CSS>body>footer ".footer from header.css"]] 46 | ) 47 | 48 | (defcomponent root [] 49 | [:div 50 | [header] 51 | [body]] 52 | ) 53 | 54 | (defn mount-root [] 55 | (r/render [root] 56 | (.getElementById js/document "app"))) 57 | 58 | (defn ^:export init [] 59 | (do 60 | (println "Loading") 61 | (mount-root) 62 | )) 63 | 64 | (init) -------------------------------------------------------------------------------- /src/clojurescript_css_modules_demo/js/css.cljs: -------------------------------------------------------------------------------- 1 | (ns clojurescript-css-modules-demo.js.css 2 | (:require [cssModules] 3 | [clojure.string :as string] 4 | [cljs.reader :as reader] 5 | [clojure.walk :refer [prewalk]])) 6 | 7 | (enable-console-print!) 8 | 9 | (defn ^:private populate-css-module-classes 10 | "Walks through an element within Reagent's hiccup-style syntax to replace our CSS module syntax with the actual CSS class name(s) from the exposed cssModules foreign-lib." 11 | [e] 12 | (prewalk (fn [x] 13 | (let [key (if (keyword? x) (str (name x))) 14 | class-lookup (if (some? key) (re-find #"(?:CSS>)[^\s][a-zA-Z>-]+" key)) 15 | tree (if (some? key) (rest (string/split class-lookup #">"))) 16 | class-names (or (if (not (empty? tree)) (apply aget (js* "cssModules") tree)) ".") 17 | joined-class-names (string/replace-all class-names #" " ".") 18 | new-key (if (some? key) (keyword (string/replace-first key #"(?:CSS>)[^\s][a-zA-Z>-]+" joined-class-names))) 19 | ] 20 | 21 | (if (some? key) new-key x) 22 | ) 23 | ) e) 24 | ) 25 | 26 | (defn get-css 27 | "Helper fn that takes Reagent's hiccup syntax and replaces .CSS>x classes with their respective CSS module name(s). 28 | Reagent components can either be wrapped with this function, or you can use the defcomponent macro for cleaner syntax." 29 | [v] 30 | (into [] (map #(populate-css-module-classes %) v)) 31 | ) -------------------------------------------------------------------------------- /src/clojurescript_css_modules_demo/macros.clj: -------------------------------------------------------------------------------- 1 | (ns clojurescript-css-modules-demo.macros) 2 | 3 | (defmacro defcomponent [fname args fbody] 4 | `(defn ~fname ~args (css/get-css ~fbody))) --------------------------------------------------------------------------------