diff --git a/README.md b/README.md index 98aa48ae..ea2518e0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ # Introduction -stylefy makes it possible to define CSS styles as Clojure data and attach them into HTML elements easily without writing selectors. Styles are converted to CSS on-demand and scoped locally. This makes writing style code easy and maintainable. +stylefy makes it possible to define CSS styles as Clojure data and attach them into HTML elements easily. Styles are converted to CSS on-demand and scoped locally. This makes writing style code easy and maintainable. While being originally created with the frontend in mind, stylefy now runs on both Web browsers and servers. On the frontend, it is designed to be used along with [Reagent](https://github.com/reagent-project/reagent). stylefy uses [Garden](https://github.com/noprompt/garden) in the background for most of its CSS conversions. @@ -33,7 +33,7 @@ While being originally created with the frontend in mind, stylefy now runs on bo Add dependency: ```clj -[stylefy "2.0.0"] +[stylefy "2.1.0"] ``` # Setup @@ -103,8 +103,6 @@ Full example: (fn [] (html (index))))) ``` -If you wish to do something else with the generated CSS, you can create your own execution context. Use the implementation of `query-with-styles` as an example. - # Usage ## Creating & using styles @@ -134,7 +132,7 @@ To use it in a component, use the **use-style** function: text]) ``` -On the frontend, **use-style** adds the style into the DOM as a CSS class and returns its class name (see FAQ for more details). On the server, it saves the generated CSS into the current execution context and returns a class name. +On the frontend, **use-style** adds the style into the DOM as a CSS class on-demand (see "How it works" for more details). On the server, it returns a class name pointing to the generated CSS code. ### Passing styles to components @@ -379,9 +377,9 @@ Alternative syntax: [bs-navbar-item 3 active-index "Four"]]))) ``` -## Font-face (frontend only) +## Font-face -Call **stylefy/font-face** and the given font-face is added into the DOM as CSS code asynchronously. +Call **stylefy/font-face** and the given font-face is added into the DOM as CSS code asynchronously (frontend) or into the current execution context (backend). ```clojure (stylefy/font-face {:font-family "open_sans" @@ -390,9 +388,9 @@ Call **stylefy/font-face** and the given font-face is added into the DOM as CSS :font-style "normal"}) ``` -## Keyframes (frontend only) +## Keyframes -Call **stylefy/keyframes** and the given keyframes are added into the DOM as CSS code asynchronously. +Call **stylefy/keyframes** and the given keyframes are added into the DOM as CSS code asynchronously (frontend) or into the current execution context (backend). ```clojure (stylefy/keyframes "simple-animation" @@ -407,31 +405,30 @@ Call **stylefy/keyframes** and the given keyframes are added into the DOM as CSS :animation-iteration-count "infinite"})) ``` -## Custom tag styles (frontend only) +## Custom class names -Call **stylefy/keyframes** and the given tag style is added into the DOM as CSS code asynchronously. +As has been told, stylefy converts style definition to unique CSS classes automatically and there is no need to worry about class names. It can, however, be useful to be able to generate custom named classes for example when working with 3rd party libraries / frameworks. For this purpose, call **stylefy/class**: ```clojure -;; This generates a CSS tag selector and style for "body" element -(def body-style - {:background-color :lightyellow - :padding :5px}) +;; This generates a CSS class with the name "background-transition" and adds it into the DOM asynchronously (frontend) or into the current execution context (backend). +(stylefy/class "background-transition" + {:transition "background-color 1s"}) -(stylefy/tag "body" body-style) +;; Use the generated class in a component like any other class +[:div.background-transition] ``` +## Custom tag styles (frontend only) -## Custom class names (frontend only) - -As has been told, stylefy converts style maps to unique CSS classes automatically and there is no need to create classes manually. It can, however, be useful to be able to generate custom named classes for example when working with 3rd party libraries / frameworks. For this purpose, call **stylefy/class**, which generates a new CSS class and adds it into the DOM asynchronously. +You can generate styles for HTML tags by calling **stylefy/tag**: ```clojure -;; This generates a CSS class with the name "background-transition" and adds it into the DOM. -(stylefy/class "background-transition" - {:transition "background-color 1s"}) +;; This generates a CSS tag selector and style for "body" element and adds it into the DOM asynchronously (frontend) or into the current execution context (backend). +(def body-style + {:background-color :lightyellow + :padding :5px}) -;; Use the generated class in a component like any other class -[:div.background-transition] +(stylefy/tag "body" body-style) ``` ## Manual mode diff --git a/project.clj b/project.clj index 44fd0223..ce032acc 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject stylefy "2.0.0" +(defproject stylefy "2.1.0" :description "Library for styling UI components" :url "https://github.com/Jarzka/stylefy" :dependencies [[org.clojure/clojure "1.9.0"] diff --git a/src/cljc/stylefy/core.cljc b/src/cljc/stylefy/core.cljc index 1cdf0f4c..26b252be 100644 --- a/src/cljc/stylefy/core.cljc +++ b/src/cljc/stylefy/core.cljc @@ -1,12 +1,15 @@ (ns stylefy.core (:require [clojure.string :as str] - [stylefy.impl.hashing :as hashing] - [stylefy.impl.styles :as impl-styles] + [garden.core :refer [css]] + [garden.stylesheet :refer [at-media at-keyframes at-font-face]] + [stylefy.impl.conversion :as conversion] #?(:cljs [stylefy.impl.dom :as dom]) + [stylefy.impl.hashing :as hashing] + [stylefy.impl.log :as log] [stylefy.impl.state :as state] - [stylefy.impl.log :as log])) + [stylefy.impl.styles :as impl-styles])) -(def ^:dynamic css-in-context (atom nil)) +#?(:clj (def ^:dynamic css-in-context (atom nil))) (defn use-style "Defines a style for a component by converting the given style map in to an unique CSS class, @@ -64,7 +67,7 @@ (assert (or (map? options) (nil? options)) (str "Options should be a map or nil, got: " (pr-str options))) #?(:cljs (impl-styles/use-style! style options dom/save-style!) :clj (impl-styles/use-style! style options (fn [{:keys [hash css]}] - (swap! css-in-context assoc hash css)))))) + (swap! css-in-context assoc-in [:stylefy-classes hash] css)))))) (defn use-sub-style @@ -82,7 +85,7 @@ (str "Options should be a map or nil, got: " (pr-str options))) #?(:cljs (impl-styles/use-sub-style! style sub-style options dom/save-style!) :clj (impl-styles/use-sub-style! style sub-style options (fn [{:keys [hash css]}] - (swap! css-in-context assoc hash css)))))) + (swap! css-in-context assoc-in [:stylefy-classes hash] css)))))) (defn sub-style "Returns sub-style for a given style." @@ -131,6 +134,82 @@ (reset! state/stylefy-initialised? true) #?(:cljs (dom/update-dom)))) +(defn keyframes + "Frontend: Adds the given keyframe definition into the DOM asynchronously. + Backend: Adds the given keyframe definition into the current context. + + Identifier is the name of the keyframes. + Frames are given in the same form as Garden accepts them. + + Example: + (stylefy/keyframes \"simple-animation\" + [:from + {:opacity 0}] + [:to + {:opacity 1}])" + [identifier & frames] + (assert (string? identifier) (str "Identifier should be string, got: " (pr-str identifier))) + (let [garden-syntax (apply at-keyframes identifier frames)] + #?(:cljs (dom/add-keyframes identifier garden-syntax) + :clj (swap! css-in-context assoc-in [:keyframes identifier] (css garden-syntax))))) + +(defn font-face + "Frontend: Adds the given font-face definition into the DOM asynchronously. + Backend: Adds the given font-face definition into the current context. + + Properties are given in the same form as Garden accepts them. + + Example: + (stylefy/font-face {:font-family \"open_sans\" + :src \"url('../fonts/OpenSans-Regular-webfont.woff') format('woff')\" + :font-weight \"normal\" + :font-style \"normal\"})" + [properties] + (assert (map? properties) (str "Properties should be a map, got: " (pr-str properties))) + + (let [garden-syntax (at-font-face properties)] + #?(:cljs (dom/add-font-face garden-syntax) + :clj (swap! css-in-context assoc :font-faces + (conj (:font-faces @css-in-context) (css garden-syntax)))))) + +(defn tag + "Frontend: Creates a CSS selector for the given tag and properties and adds it into the DOM asynchronously. + Backend: Creates a CSS selector for the given tag and properties and adds it into the current context. + + Normally you should let stylefy convert your style maps to unique CSS classes by calling + use-style, instead of creating tag selectors. However, custom tag styles + can be useful for setting styles on base elements, like html or body. + + Example: + (stylefy/tag \"code\" + {:background-color \"lightyellow\"})" + [name properties] + (assert (string? name) (str "Tag name should be a string, got: " (pr-str name))) + (assert (map? properties) (str "Properties should be a map, got: " (pr-str properties))) + + (let [tag-as-css (conversion/style->css {:props properties :custom-selector name})] + #?(:cljs (dom/add-tag tag-as-css) + :clj (swap! css-in-context assoc-in [:tags name] tag-as-css)))) + +(defn class + "Frontend: Creates a CSS class with the given name and properties and adds it into the DOM asynchronously. + Backend: Creates a CSS class with the given name and properties and adds it into the the current context. + + Normally you should let stylefy convert your style maps to unique CSS classes by calling + use-style. Thus, there is usually no need to create customly named classes when using stylefy, + unless you work with some 3rd party framework. + + Example: + (stylefy/class \"enter-transition\" + {:transition \"background-color 2s\"})" + [name properties] + (assert (string? name) (str "Name should be a string, got: " (pr-str name))) + (assert (map? properties) (str "Properties should be a map, got: " (pr-str properties))) + + (let [class-as-css (conversion/style->css {:props properties :custom-selector (conversion/class-selector name)})] + #?(:cljs (dom/add-class class-as-css) + :clj (swap! css-in-context assoc-in [:classes name] class-as-css)))) + ; ; Backend only ; @@ -144,76 +223,18 @@ [query] (binding [stylefy.core/css-in-context (atom nil)] (let [result (query) - result-with-styles-attached (str/replace - result - #"_stylefy-server-styles-content_" - (apply str (vals @css-in-context)))] + css (str + (apply str (:font-faces @css-in-context)) + (apply str (vals (:keyframes @css-in-context))) + (apply str (vals (:tags @css-in-context))) + (apply str (vals (:classes @css-in-context))) + (apply str (vals (:stylefy-classes @css-in-context)))) + result-with-styles-attached (str/replace result #"_stylefy-server-styles-content_" css)] result-with-styles-attached)))) ; ; Frontend only ; -#?(:cljs - (defn keyframes - "Adds the given keyframe definition into the DOM asynchronously. - Identifier is the name of the keyframes. - Frames are given in the same form as Garden accepts them. - - Example: - (stylefy/keyframes \"simple-animation\" - [:from - {:opacity 0}] - [:to - {:opacity 1}])" - [identifier & frames] - (assert (string? identifier) (str "Identifier should be string, got: " (pr-str identifier))) - (apply dom/add-keyframes identifier frames))) - -#?(:cljs - (defn font-face - "Adds the given font-face definition into the DOM asynchronously. - Properties are given in the same form as Garden accepts them. - - Example: - (stylefy/font-face {:font-family \"open_sans\" - :src \"url('../fonts/OpenSans-Regular-webfont.woff') format('woff')\" - :font-weight \"normal\" - :font-style \"normal\"})" - [properties] - (assert (map? properties) (str "Properties should be a map, got: " (pr-str properties))) - (dom/add-font-face properties))) - -#?(:cljs - (defn tag - "Creates a CSS selector for the given tag and properties and adds it into the DOM asynchronously. - - Normally you should let stylefy convert your style maps to unique CSS classes by calling - use-style, instead of creating tag selectors. However, custom tag styles - can be useful for setting styles on base elements, like html or body. - - Example: - (stylefy/tag \"code\" - {:background-color \"lightyellow\"})" - [name properties] - (assert (string? name) (str "Tag name should be a string, got: " (pr-str name))) - (assert (map? properties) (str "Properties should be a map, got: " (pr-str properties))) - (dom/add-tag name properties))) - -#?(:cljs - (defn class - "Creates a CSS class with the given name and properties and adds it into the DOM asynchronously. - - Normally you should let stylefy convert your style maps to unique CSS classes by calling - use-style. Thus, there is usually no need to create customly named classes when using stylefy, - unless you work with some 3rd party framework. - - Example: - (stylefy/class \"enter-transition\" - {:transition \"background-color 2s\"})" - [name properties] - (assert (string? name) (str "Name should be a string, got: " (pr-str name))) - (assert (map? properties) (str "Properties should be a map, got: " (pr-str properties))) - (dom/add-class name properties))) #?(:cljs (defn prepare-styles diff --git a/src/cljs/stylefy/impl/dom.cljs b/src/cljs/stylefy/impl/dom.cljs index 773ba1e9..b31bb1f1 100644 --- a/src/cljs/stylefy/impl/dom.cljs +++ b/src/cljs/stylefy/impl/dom.cljs @@ -91,7 +91,8 @@ (when-not @dom-update-requested? (reset! dom-update-requested? true) (go - (update-dom))))) + (update-dom)) + nil))) (defn init-multi-instance [{:keys [multi-instance] :as options}] (let [base-node (:base-node multi-instance) @@ -130,30 +131,22 @@ ;; itself if the "CSS in DOM" state of this specific style hash is changed. (boolean @(get @styles-in-dom style-hash))) -(defn add-keyframes [identifier & frames] - (let [garden-definition (apply at-keyframes identifier frames)] - (swap! keyframes-in-use assoc identifier (css garden-definition)) - (request-asynchronous-dom-update) - garden-definition)) - -(defn add-font-face [properties] - (let [garden-definition (at-font-face properties)] - (swap! font-faces-in-use conj {::css (css garden-definition)}) - (request-asynchronous-dom-update) - garden-definition)) - -(defn add-tag [name properties] - (let [custom-tag-definition {::tag-name name ::tag-properties properties}] - (swap! custom-tags-in-use conj {::css (conversion/style->css - {:props (::tag-properties custom-tag-definition) - :custom-selector (::tag-name custom-tag-definition)})}) - (request-asynchronous-dom-update) - custom-tag-definition)) - -(defn add-class [name properties] - (let [custom-class-definition {::class-name name ::class-properties properties}] - (swap! custom-classes-in-use conj {::css (conversion/style->css - {:props (::class-properties custom-class-definition) - :custom-selector (conversion/class-selector (::class-name custom-class-definition))})}) - (request-asynchronous-dom-update) - custom-class-definition)) +(defn add-keyframes [identifier garden-syntax] + (swap! keyframes-in-use assoc identifier (css garden-syntax)) + (request-asynchronous-dom-update) + nil) + +(defn add-font-face [garden-syntax] + (swap! font-faces-in-use conj {::css (css garden-syntax)}) + (request-asynchronous-dom-update) + nil) + +(defn add-tag [tag-css] + (swap! custom-tags-in-use conj {::css tag-css}) + (request-asynchronous-dom-update) + nil) + +(defn add-class [class-as-css] + (swap! custom-classes-in-use conj {::css class-as-css}) + (request-asynchronous-dom-update) + nil) diff --git a/test/stylefy/tests/ssr_test.clj b/test/stylefy/tests/ssr_test.clj index a8c26d77..a617f6f2 100644 --- a/test/stylefy/tests/ssr_test.clj +++ b/test/stylefy/tests/ssr_test.clj @@ -71,34 +71,68 @@ (defn init-stylefy [] (stylefy/init)) +(defn constant-styles [] + (stylefy/keyframes "simple-animation" + [:from + {:opacity 0}] + [:to + {:opacity 1}]) + + (stylefy/font-face {:font-family "open_sans" + :src "url('../fonts/OpenSans-Regular-webfont.woff') format('woff')" + :font-weight "normal" + :font-style "normal"}) + + (stylefy/tag "code" + {:background-color "lightyellow"}) + + (stylefy/class "enter-transition" + {:transition "background-color 2s"})) + (defn index-query [] + (stylefy/query-with-styles - (fn [] (html (example-component))))) + (fn [] + (constant-styles) + (html (example-component))))) (deftest query-with-styles (init-stylefy) - (let [result (index-query)] - ; HTML document is rendered - (is (str/includes? result "")) - ; Styles are rendered - (is (str/includes? result "