Support styling based on ancestor class (.parent &)
shanberg opened this issue · 18 comments
A common issue in component styling is the ability to modify the style of the current object based on the state of one of its ancestors.
.my-component { color: blue }
.os-macos .my-component { color: green }
.os-windows .my-component { color: orange }
The &
selector seems to only implement part of the traditional SCSS/Less featureset. I can use it to create selectors like &:hover
-> .my-component:hover
, but not :hover > &
-> :hover > .mycomponent
.
I haven't been able to replicate this kind of styling workflow using stylefy.
Have you tried using stylefy's manual mode to write this kind of selector manually? Manual mode can be used to write custom CSS using Garden syntax. The parent selector is also supported in Garden:
As in Less/Sass, Garden also supports selectors prefixed with the & character allowing you to reference a parent selector:
Another possible option is to have os-macos
and os-windows
as Clojure data and pass them as parameters for styling functions.
Here's what I get with manual mode and when using garden selector:
(def my-component-style
{:color "red"
::stylefy/manual [[(selectors/& :.test) {:color "blue"}]
[(:.test selectors/&) {:color "blue"}]
["&.test" {:color "blue"}]
[".test &" {:color "blue"}]]})
which evaluates to:
._stylefy_2087609553 {
color: red;
}
._stylefy_2087609553.test {
color: blue;
}
._stylefy_2087609553.test {
color: blue;
}
._stylefy_2087609553 .test & {
color: blue;
}
and what I need instead is
.test ._stylefy_2087609553 {
color: blue;
}
I'm currently working around this by adding classes directly to the component, but there are potentially very many cases where I'd like to use this format, and subscribing to the same data in many parts of our application is pretty hairy.
I see, stylefy scopes manually written selectors inside the current component, so there is really no (easy) way to select anything outside of it. This is a design choice, and for the most part I see it as a benefit, because it helps keeping styles scoped locally to the current component.
However, perhaps it would be handy to override the default scoping in these kind of situations - maybe there should be an option to define the current scope of the style. I have to think about this.
I have been thinking of a possible solution for this.
Media queries are used to create a new scope for a style, i.e. we define a style which is activated when the defined media query is active. We could use the same idea with custom scopes; we define a style which is activated when the defined scope (CSS selector) is active.
For example: (not implemented anywhere, just an idea):
(def style-map
{:color :grey
::stylefy/scope [[[:.os-macos] {:color "blue"}]]
[[:.os-windows] {:color "purple"}]]]})
::stylefy/scope
should be a vector of scopes. A scope is a vector in which:
- The first element is a CSS selector which defines the scope.
The autogenerated stylefy class name is automatically appended at the end of itEDIT: This can be a bit tricky with more complex selectors. Maybe we should only accept simple selectors, like "inside class A which is inside element B". - The second element is a style map which is used for the current scope
So (use-style style-map)
would output the following CSS:
._stylefy_xxxxxxx {
color: grey;
}
.os-macos ._stylefy_xxxxxxx {
color: blue;
}
.os-windows ._stylefy_xxxxxxx {
color: purple;
}
I think this could work in a simple case like this, not sure about more complex ones. Also, I'm not sure if we could use scopes and media queries together without creating ambiguous style maps. ::stylefy/scope
feels like a more advanced way of using ::stylefy/media
, so these probably would not work together (you could and should define your media queries manually in scope selector).
EDIT: I think scopes and media queries could also be used together:
Input:
(def style-map
{:color :grey
::stylefy/media {{:max-with "20rem"} {:color "yellow"
::stylefy/scope [[[:.os-macos] {:color "black"}]]
[[:.os-windows] {:color "white"}]]]}}
::stylefy/scope [[[:.os-macos] {:color "blue"}]]
[[:.os-windows] {:color "purple"}]]]})
Output:
._stylefy_xxxxxxx {
color: grey;
}
.os-macos ._stylefy_xxxxxxx {
color: blue;
}
.os-windows ._stylefy_xxxxxxx {
color: purple;
}
@media (max-width: 20rem) {
._stylefy_xxxxxxx {
color: yellow;
}
.os-macos ._stylefy_xxxxxxx {
color: black;
}
.os-windows ._stylefy_xxxxxxx {
color: white;
}
}
What do you think about this?
Maybe we should only accept simple selectors, like "inside class A which is inside element B"
This makes me a little nervous. It sounds like the following nesting would be impossible:
(def style-map
{:color "red"
::stylefy/scope [[[:.os-macos] {:color "blue"}
[:.close-button {:color "yellow"}]]
[[:.os-windows] {:color "yellow"}
[:.close-button {:color "blue"}]]]})
And perhaps I'd have to create separate maps like (def close-button-style)
with the same set of scopes to handle this case.
Would manual-style selectors be available in scopes, e.g.?
(def style-map
{:color "red"
::stylefy/scope [[".os-macos > .app-toolbar:hover"] {:color "green"}]})
...
Ultimately, virtually any limit on selector complexity will become a future pain point, but a limited solution would get me past my current roadblock, and I'll be grateful for that.
Ultimately, virtually any limit on selector complexity will become a future pain point
This is true. Limiting selector complexity would solve simple cases, but there will be cases in which simple selectors are simply not enough.
Would manual-style selectors be available in scopes, e.g.?
This could be a good way to begin with. I have also been thinking of enabling support for manual-style selectors when using stylefy's manual mode, since some folks may find it easier / more advanced than writing Garden-selectors.
Since Garden uses vector-based selectors, I think we can simplify the scope syntax a bit when using string selectors. The first element in scope vector could be the string selector itself:
(def style-map
{:color "red"
::stylefy/scope [[".os-macos > .app-toolbar:hover" {:color "green"}]
[".os-windows > .app-toolbar:hover" {:color "blue"}]]})
Now it should be easy to append the generated class name at the end of the selector to create the final selector:
.os-macos > .app-toolbar:hover ._stylefy_xxxxxxx
.
Later, if we find a reliable way to support Garden selectors, we have to check the syntax of the scope vector. If the first element is a string and the second element is a map, we are using manual style, otherwise, it is a Garden selector.
EDIT: I noticed that this is also valid Garden syntax:
(garden.core/css [".os-macos > .app-toolbar:hover" {:color "green"}])
=> ".os-macos > .app-toolbar:hover {\n color: green;\n}"
So... I think we can assume that if the item in scope is a vector, it is a valid Garden selector, but the selector itself should be written manually (the first element is a string and the second element is a map), so that we can easily append the generated class name at the end of it. But I'm not sure if we can ever support "real" Garden syntax if we have this limitation, and there is also a risk that we are going to run into some syntax conflict at some point.
Another option is to make the following assumption:
If the item in scope is a vector, we assume it is valid Garden syntax and can be written the way you like. However, this is not going to be supported yet, i.e. this would throw an error in the first implementation.
If the item in scope is a map, it should explicitly tell what syntax you are using:
(def style-map
{:color "red"
::stylefy/scope [{:css [".os-macos > .app-toolbar:hover" {:color "green"}]}
; Explicit garden syntax, not going to be supported yet
{:garden [:.os-windows [:.app-toolbar [:&:hover {:color "blue"}]]]}
; Vector items are assumed to be Garden syntax (also not supported yet)
[:.os-windows [:.app-toolbar [:&:hover {:color "blue"}]]]]})
This would sound like a future-proof solution to me.
About Garden syntax support. I think it should be possible to achieve with the following algorithm:
- Check every item in the Garden vector
- If you find a style map, wrap it with the current class name
So this:
{:color "red"
::stylefy/scope [[:.os-macos {:color "blue"}]
[:.os-windows {:color "yellow"}
[:.what [:.ever {:color "purple"}]]]]}
Would end up looking like this after the conversion:
{:color "red"
::stylefy/scope [[:.os-macos [:._stylefy_xxxxxx {:color "blue"}]]
[:.os-windows [:._stylefy_xxxxxx {:color "yellow"}]
[:.what [:.ever [:._stylefy_xxxxxx {:color "purple"}]]]]]}
Which would end up like this in CSS:
.os-macos ._stylefy_xxxxxx {
color: blue;
}
.os-windows ._stylefy_xxxxxx {
color: yellow;
}
.what .ever ._stylefy_xxxxxx {
color: purple;
}
I also tested a few more complex selectors and they ended up looking ok: stylefy's autogenerated class name is always the last part of the selector. So, maybe we don't need the manual way after all. We could support Garden syntax, and you can always use string selectors as part of the Garden selector if you prefer.
This looks like an improvement to me.
It looks like stylefy/scope
is always prepending a set of selectors to the autogenerated classname. Is it possible to also append selectors, e.g. to return this: .os-windows ._stylefy_xxxxx .child-of_stylefy_xxxxx { color: blue }
?
What I'm accustomed to is being able to use &
in any part of a nested selector to represent the parent selector, so I'm most excited by whatever gets me closest to that behavior.
I do not think this would be possible with the currently designed implementation; the autogenerated class name of the current style would always be the last element in the scoped CSS selector. So ::stylefy/scope
would always mean "this style in this context". Child elements could be scoped separately. Imo it would make more sense to do it that way. Or... you could possibly use ::stylefy/manual
to style them in the parent element's scoped style map.
{:color "red"
::stylefy/scope [[:.os-macos {:color "blue"
::stylefy/manual [[:.child {:color "green"}]]}]]]}
._stylefy_xxxxxx {
color: red;
}
.os-macos ._stylefy_xxxxxx {
color: blue;
}
.os-macos ._stylefy_xxxxxx .child {
color: green;
}
Also, I'm not sure if it is a good idea to ever use autogenerated class names in CSS selectors since those names can easily change. It would be a better idea to attach additional "human readable" class name to your elements and refer to them instead.
I had a little bit of time today to create a minimal implementation of this feature. It's available on the feature/scope
branch. It works as I explained in my previous post, except that the scoped style map does not yet support stylefy's special keywords (this turned out to be a bit trickier than I expected because Garden conversion needs the full selector and style map, I cannot convert only the selector or the style map separately).
Vendor prefixes, ::stylefy/mode
and ::stylefy/manual
can now be used inside scoped style map:
(def scoped-box
{:font-weight :bold
::stylefy/scope [[:.scoped-box {:color "red"
::stylefy/mode {:hover {:color "yellow"}}
::stylefy/manual [[:.green-text-in-scoped-box {:color "green"}]]}]]})
Output:
._stylefy_974852753 {
font-weight: bold;
}
.scoped-box ._stylefy_974852753 {
color: red;
}
.scoped-box ._stylefy_974852753:hover {
color: yellow;
}
.scoped-box ._stylefy_974852753 .green-text-in-scoped-box {
color: green;
}
This is still pretty much experimental work and should not be used in production. Still, it would be nice to get some feedback. :)
I'm having difficulty building this locally to give it a rigorous test, but I'm happy with the syntax you've described and the examples you've demonstrated. This looks like it entirely covers my use cases.
Thanks so much for pursuing this.
I think you should be able to test it by cloning the repo, switching to feature/scope
branch and running lein install
. After that, require [stylefy "3.1.0-SNAPSHOT"]
.
Anyway, I'm probably going to release a public beta at some point - when the work has progressed a bit further.
I've just tested it and it’s working great so far. It’s a relief to be able to set a few classes at the app root and use them much farther down.
Scoping now supports media queries. You can write them either inside the scoping 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"}])]}]]}
Output
._stylefy_-281407580 {
font-weight: bold;
}
.scoped-box ._stylefy_-281407580 {
color: red;
}
.scoped-box ._stylefy_-281407580:hover {
color: yellow;
}
.scoped-box ._stylefy_-281407580 .special-text-in-scoped-box {
color: green;
}
@media (max-width: 500px) {
.scoped-box ._stylefy_-281407580 .special-text-in-scoped-box {
color: purple;
}
}
...or have a ::stylefy/media
style definition with scoping rules. The CSS output is a bit different, but the end result is virtually the same:
{:font-weight :bold
::stylefy/scope [[:.scoped-box {:color "red"
::stylefy/mode {:hover {:color "yellow"}}
::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"}]]}]]}}}
Outputs:
._stylefy_-646023592 {
font-weight: bold;
}
.scoped-box ._stylefy_-646023592 {
color: red;
}
.scoped-box ._stylefy_-646023592:hover {
color: yellow;
}
.scoped-box ._stylefy_-646023592 .special-text-in-scoped-box {
color: green;
}
@media (max-width: 500px) {
._stylefy_-646023592 {}
}
@media (max-width: 500px) {
.scoped-box ._stylefy_-646023592 {}
.scoped-box ._stylefy_-646023592 .special-text-in-scoped-box {
color: purple;
}
}
This feature is looking good so I opened a PR for it:
https://github.com/Jarzka/stylefy/pull/60/files
I'm planning making this official soon if no issues are found.
This feature is now live in 3.1.0
I updated the scoping examples in the README file. I added an example in which media queries are used directly inside ::stylefy/scope
.
Since this feature is now done, I'll close this issue. If there is something related to this, let's discuss that in a new thread.