Skip to content

Commit

Permalink
Merge pull request #60 from Jarzka/feature/scope
Browse files Browse the repository at this point in the history
Style scoping
  • Loading branch information
Jarzka authored Jun 7, 2021
2 parents 7200aae + e98d1ec commit 4bed475
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 31 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# 3.1.0

June 7, 2021

- Adds a new feature: `::stylefy/scope`, which can be used to define styles that are applied only when the current
element is in some specific scope. See README.md for more info.

# 3.0.0

April 9, 2021
Expand Down
49 changes: 45 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ While being originally created with the frontend in mind, stylefy now runs on bo

- Define styles as Clojure data – all important CSS features are supported: pseudo-classes, pseudo-elements, keyframes, font-faces, media queries, feature queries...
- Use any 3rd party CSS code along with stylefy
- Manual mode can be used for styling 3rd party components and resolving corner cases in which complex CSS selectors are needed
- Manual mode: stylize 3rd party components and resolve corner cases in which complex CSS selectors are needed
- Scoping: define styles that are applied only when the current element is in some specific scope
- Vendor prefixes are supported both locally and globally
- CSS caching on the frontend using local storage (optional)
- Small and simple core API which is easy to setup
Expand All @@ -35,7 +36,7 @@ stylefy consists of modules that are optimised for specific UI libraries / frame
First, add stylefy core as a dependency:

```clj
[stylefy "3.0.0"]
[stylefy "3.1.0"]
```

Then, based on the UI library you are using, add a corresponding module. The main purpose of the module is to handle asynchronous DOM updates when components are rendered.
Expand Down Expand Up @@ -358,7 +359,7 @@ Define how your style looks on various screen sizes:
[:p "This is column 3"]]])
```

You can also use modes and vendor prefixes inside media query style map.
stylefy features supported in media queries: modes and vendor prefixes.

For syntax help, see Garden's [documentation](https://github.com/noprompt/garden/wiki/Media-Queries).

Expand All @@ -385,7 +386,7 @@ Define how your style looks when certain CSS features are supported by the brows

```

You can use modes, media queries, and vendor prefixes inside feature query style map.
stylefy features supported in feature queries: modes, media queries and vendor prefixes.

## 3rd party classes

Expand Down Expand Up @@ -537,6 +538,46 @@ Notice that Garden's selector syntax can contain strings, so purely string-based
::stylefy/manual [["> .box:hover" {:color "black"}]]})
```

## Scoping

Scoping can be used to define styles that are applied only when the current element is in some specific scope.

```clojure
(def style
{:font-weight :bold
::stylefy/scope [[:.scoped-box
; These additional style definitions are applied only
; when the current element is inside of .scoped-box
{:color "red"
::stylefy/mode {:hover {:color "yellow"}}
::stylefy/manual [[:.green-text-in-scoped-box {:color "green"}]]}]]})
```

When scoping styles, media queries can be used either inside the scoped style map using manual mode...

```clojure
(def style)
{:font-weight :bold
::stylefy/scope [[:.scoped-box {:color "red"
::stylefy/manual [[:.green-text-in-scoped-box {:color "green"}]
(at-media {:max-width "500px"} [:.green-text-in-scoped-box {:color "purple"}])]}]]} )
```

...or by using `::stylefy/media` in the parent style map:

```clojure
(def style
{:font-weight :bold
::stylefy/scope [[:.scoped-box {:color "red"
::stylefy/manual [[:.special-text-in-scoped-box {:color "green"}]]}]]
::stylefy/media {{:max-width "500px"}
{::stylefy/scope [[:.scoped-box {::stylefy/manual [[:.special-text-in-scoped-box {:color "purple"}]]}]]}}})
```

stylefy features supported in scoped style map: modes, manual mode, vendor prefixes (must be defined in parent style map).

For syntax help, see Garden's [documentation](https://github.com/noprompt/garden/wiki/Syntax).

## <a name="caching"></a> Style caching (frontend)

stylefy supports style caching for styles generated on the frontend via HTML5 local storage. The converted CSS code is added into local storage and loaded from there when the page is reloaded.
Expand Down
36 changes: 36 additions & 0 deletions examples/reagent/src/cljs/stylefy/examples/reagent/main.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
(:require
[garden.units :as gu]
[garden.color :as gc]
[garden.stylesheet :refer [at-media]]
[reagent.core :as r]
[reagent.dom :as reagent-dom]
[stylefy.examples.reagent.styles :as styles]
Expand Down Expand Up @@ -196,6 +197,38 @@
:background-image (url "images/background.jpg")})
"Defined with pc unit, height with rem unit, color with rgb, background as a custom defcssfn function."]])

(def scoped-box-wrapper {:border "1px solid black"
:margin-top "1rem"
:padding "1rem"})

(def scoped-box-element-with-manual-media-query
{:font-weight :bold
::stylefy/scope [[:.scoped-box {:color "red"
::stylefy/mode {:hover {:color "yellow"}}
::stylefy/manual [[:.special-text-in-scoped-box {:color "green"}]
(at-media {:max-width "500px"} [:.special-text-in-scoped-box {:color "purple"}])]}]]})

(def scoped-box-element-with-stylefy-media-query
{:font-weight :bold
::stylefy/media {{:max-width "500px"}
{::stylefy/scope [[:.scoped-box {::stylefy/manual [[:.special-text-in-scoped-box {:color "purple"}]]}]]}}
::stylefy/scope [[:.scoped-box {:color "red"
::stylefy/mode {:hover {:color "yellow"}}
::stylefy/manual [[:.special-text-in-scoped-box {:color "green"}]]}]]})

(defn- scoped-box []
[:div
[:div (use-style scoped-box-element-with-manual-media-query)
[:p "This text is only bold since it is not inside scoped box"]]
[:div.scoped-box (use-style scoped-box-wrapper)
[:div (use-style scoped-box-element-with-manual-media-query)
[:p "This text is both bold and red since it is in scoped box. Hovering makes it yellow."]
[:p.special-text-in-scoped-box "This text is bold and green, as defined by scoped box style and it's manual mode. On small screens, the text turns purple, as defined by scoped's styles manual media query."]]]
[:div.scoped-box (use-style scoped-box-wrapper)
[:div (use-style scoped-box-element-with-stylefy-media-query)
[:p "This text is both bold and red since it is in scoped box. Hovering makes it yellow."]
[:p.special-text-in-scoped-box "This text is bold and green, as defined by scoped box style and it's manual mode. On small screens, the text turns purple, as defined by stylefy's media query."]]]])

(def background-box-sorted (sorted-map
:width "100%"
:height "20rem"
Expand Down Expand Up @@ -305,6 +338,9 @@

[garden-units]

[:h2 "Scoped styles"]
[scoped-box]

[:h2 "Key order"]
[:p "If CSS shorthands are used, the order of CSS key properties is important. If we use a regular Clojure map, the order of keys can change in the final CSS output."]
[:p "This specific style map renders incorrectly (some background properties are defined before the background itself and these are ignored by the browser):"]
Expand Down
2 changes: 1 addition & 1 deletion project.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(defproject stylefy "3.0.0"
(defproject stylefy "3.1.0"
:description "Library for styling UI components"
:url "https://github.com/Jarzka/stylefy"
:dependencies [[org.clojure/clojure "1.9.0"]
Expand Down
10 changes: 5 additions & 5 deletions src/cljc/stylefy/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,16 @@
::auto-prefix A set of style properties that should be prefixed with ::vendors.
::with-classes A collection of additional class names that should always be used with
this style definition.
::manual Manual mode can be used to style child elements with manually written CSS selectors
using Garden syntax. It should be used only for styling 3rd party components and
resolving corner cases in which complex CSS selectors are needed.
::scope Can be used to define styles that are applied only when the current element is in some specific scope.
Additional features:
::class-prefix Custom prefix for generated class names. If not given, the default prefix will be used.
Custom prefix can be used for debugging and automatic software testing purposes.
Note that you need to set custom class prefixes on in the init function.
::manual Manual mode can be used to style child elements with manually written CSS selectors
using Garden syntax. It should be used only for styling 3rd party components and
resolving corner cases in which complex CSS selectors are needed.
For the most part, it is recommended to use ::sub-styles.
Note that you need to turn custom class prefixes on in the init function.
Options is an optional map, which contains HTML attributes (:class, :href, :src etc.)."
([style] (use-style style {}))
Expand Down
80 changes: 65 additions & 15 deletions src/cljc/stylefy/impl/conversion.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,52 @@
garden-pseudo-classes))]
css-class))

(defn prepare-manual-style-map [style-map]
(walk #(if (map? %)
(utils/remove-special-keywords %)
%)
identity
style-map))

(defn- handle-scoped-style-map [props scope]
(let [scoped-style [scope (utils/remove-special-keywords props)]
garden-pseudo-classes (convert-stylefy-modes-to-garden props)
stylefy-manual-styles (:stylefy.core/manual props)]
(apply conj scoped-style
(concat
garden-pseudo-classes
(mapv prepare-manual-style-map stylefy-manual-styles)))))

(defn- find-and-handle-scoped-style-map [item scope]
(cond
(map? item)
(handle-scoped-style-map item scope)

(vector? item)
(mapv #(find-and-handle-scoped-style-map % scope) item)

:else item))

(defn- convert-scoped-styles
"Converts stylefy/scope definition into CSS selector.
stylefy features supported in scoped style map:
- modes
- manual mode
- vendor prefixes (must be defined in the parent style map)"
[{:keys [props hash custom-selector] :as _style} options]
(when-let [stylefy-scoped-styles (:stylefy.core/scope props)]
(let [css-parent-selector (or custom-selector (class-selector hash))
css-scoped-styles (map
(fn [scoping-rule]
(let [selector-and-props (find-and-handle-scoped-style-map scoping-rule css-parent-selector)
garden-vendors (convert-stylefy-vendors-to-garden props)
garden-options (or (merge options garden-vendors) {})
css-selector (css garden-options selector-and-props)]
css-selector))
stylefy-scoped-styles)]
(apply str css-scoped-styles))))

(defn- convert-media-queries
"Converts stylefy/media definition into CSS media query.
Expand All @@ -65,19 +111,25 @@
stylefy/manual is not supported here since one can use it to create
media queries."
; TODO Media queries could also be defined in a vector (just like modes can be defined as a map or vector)
[{:keys [props hash custom-selector] :as _style} options]
(when-let [stylefy-media-queries (:stylefy.core/media props)]
(let [css-selector (or custom-selector (class-selector hash))
css-media-queries (map
(fn [media-query]
(let [media-query-props (get stylefy-media-queries media-query)
media-query-css-props (utils/remove-special-keywords media-query-props)
scoped-styles-garden (map
(fn [scoping-rule]
(find-and-handle-scoped-style-map scoping-rule css-selector))
(:stylefy.core/scope media-query-props))
garden-class-definition [css-selector media-query-css-props]
garden-pseudo-classes (convert-stylefy-modes-to-garden media-query-props)
garden-vendors (convert-stylefy-vendors-to-garden media-query-props)
garden-options (or (merge options garden-vendors) {})]
(css garden-options (at-media media-query (into garden-class-definition
garden-pseudo-classes)))))
(css garden-options [(at-media media-query (into garden-class-definition
garden-pseudo-classes))
(at-media media-query scoped-styles-garden)])))
(keys stylefy-media-queries))]
(apply str css-media-queries))))

Expand All @@ -88,7 +140,7 @@
- modes
- media queries
- vendor prefixes"
; TODO Manual mode should also be supported here
; TODO Manual mode and scoping should also be supported here
[{:keys [props hash custom-selector] :as _style} options]
(when-let [stylefy-supports (:stylefy.core/supports props)]
(let [css-selector (or custom-selector (class-selector hash))
Expand Down Expand Up @@ -124,16 +176,11 @@
(when-let [stylefy-manual-styles (:stylefy.core/manual props)]
(let [css-parent-selector (or custom-selector (class-selector hash))
css-manual-styles (map
(fn [manual-style]
(let [manual-selector-and-css-props (walk #(if (map? %)
(utils/remove-special-keywords %)
%)
identity
manual-style)
garden-style-definition (into [css-parent-selector] [manual-selector-and-css-props])
css-class (css options garden-style-definition)]
css-class))
stylefy-manual-styles)]
(fn [manual-style]
(let [selector-and-props (into [css-parent-selector] [(prepare-manual-style-map manual-style)])
css-class (css options selector-and-props)]
css-class))
stylefy-manual-styles)]
(apply str css-manual-styles))))

(defn style->css
Expand All @@ -143,13 +190,16 @@
(let [css-class (convert-base-style-into-class style options)
css-media-queries (convert-media-queries style options)
css-feature-queries (convert-feature-queries style options)
css-scoped-styles (convert-scoped-styles style options)
css-manual-styles (convert-manual-styles style options)]
; Order is important so that more specific styles properly overwrite the previous ones.
(str
; Base style definition comes first:
css-class
; Media queries themselves have no specificity, but they appear after class so that
; the rules can be overwritten with the same selector.
; Scoped rules:
css-scoped-styles
; Media queries themselves have no specificity, but they appear after class and scope so that
; these rules can be overwritten with the same selectors.
css-media-queries
; Feature queries:
css-feature-queries
Expand Down
13 changes: 8 additions & 5 deletions src/cljc/stylefy/impl/styles.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
(let [contains-media-queries? (some? (:stylefy.core/media style))
contains-feature-queries? (some? (:stylefy.core/supports style))
contains-manual-mode? (some? (:stylefy.core/manual style))
contains-scoping? (some? (:stylefy.core/scope style))
excluded-modes #{:hover}
modes (:stylefy.core/mode style)
mode-names (cond
Expand All @@ -98,11 +99,13 @@
contains-modes-not-excluded? (seq (filter (comp not excluded-modes) mode-names))
inline-style (-> style
(utils/remove-special-keywords)
(conversion/garden-units->css))]
(if (or contains-media-queries?
contains-feature-queries?
contains-manual-mode?
contains-modes-not-excluded?)
(conversion/garden-units->css))
inline-style-not-supported? (or contains-media-queries?
contains-feature-queries?
contains-manual-mode?
contains-scoping?
contains-modes-not-excluded?)]
(if inline-style-not-supported?
(merge return-map {:style (merge inline-style {:visibility "hidden"})})
(merge return-map {:style inline-style}))))
:clj return-map)))
Expand Down
Loading

0 comments on commit 4bed475

Please sign in to comment.