From e61767eccab8999f13ac74e90bd7ea23cab7dbb6 Mon Sep 17 00:00:00 2001 From: Jari Hanhela Date: Sat, 30 May 2020 21:46:38 +0300 Subject: [PATCH 1/5] Add SSR support for font-faces, keyframes, custom tag and custom class selectors --- src/cljc/stylefy/core.cljc | 161 ++++++++++++++++++-------------- src/cljs/stylefy/impl/dom.cljs | 49 +++++----- test/stylefy/tests/ssr_test.clj | 86 +++++++++++------ 3 files changed, 172 insertions(+), 124 deletions(-) diff --git a/src/cljc/stylefy/core.cljc b/src/cljc/stylefy/core.cljc index 8ee02774..8776a2f5 100644 --- a/src/cljc/stylefy/core.cljc +++ b/src/cljc/stylefy/core.cljc @@ -1,10 +1,13 @@ (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)) @@ -65,7 +68,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 @@ -83,7 +86,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." @@ -132,6 +135,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 properties) + :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 ; @@ -145,76 +224,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 "