rtfeldman / elm-css

Typed CSS in Elm.
https://package.elm-lang.org/packages/rtfeldman/elm-css/latest
BSD 3-Clause "New" or "Revised" License
1.24k stars 197 forks source link

Styling children on hover using Html.Styled? #338

Open edkv opened 6 years ago

edkv commented 6 years ago

Consider the following example:

div
    [ css 
        [ hover 
            [ backgroundColor black
            ]
        ]
    ]
    [ span [] [ text "Hello" ] 
    , span [] [ text " world!"]
    ]

Now imagine that we also need to change the font color of the first span on hover over that div. I tried to use Css.Foreign inside the css function and it actually works:

div
    [ css 
        [ hover 
            [ backgroundColor black
            , Css.Foreign.children
                [ Css.Foreign.class "test"
                    [ color white
                    ]
                ]
            ]
        ]
    ]
    [ span [ class "test" ] [ text "Hello" ] 
    , span [] [ text " world!"]
    ]

We'll get something like ._1eea206b:hover > .test. At first it seems that this selector isn't globally scoped and we only need to make sure that our class is unique among children. I was going to propose that selector functions should be moved back to the Css module because of such use cases. But it actually can be harmful. For example if you render some third-party view inside that div and its top-level element also has a .test class, you will break its styles.

The same applies to focus and similar use cases. Also it's not only about styling children, sometimes it's useful to style siblings as well (input:focus + label or input[type="password"]:-webkit-autofill + label, etc).

Are there any recommendations/ideas on how to deal with such situations?

rtfeldman commented 6 years ago

But it actually can be harmful. For example if you render some third-party view inside that div and its top-level element also has a .test class, you will break its styles.

Yup. Such are the dangers of global selectors! This is why I called the module Css.Foreign instead of Css.Global - I recommend using it only for styling DOM nodes you do not control.

I understand that maintaining a hover state in your Elm Model may seem like an unnecessary amount of effort to avoid using a global selector, but the upside is that you don't have to worry about it breaking your styles in surprising ways in the future. 😄

sporto commented 6 years ago

I'm hitting the same issue, before we were doing everything by using Html.CssHelpers.withNamespace. But I understand that this is not the recommend way anymore. Because something to do with toString going away, right?

Targeting descendants is a common thing. But putting hover state in Elm is an unnecessary complex solution when we already have :hover. And hover is not the only case when targeting descendants is needed.

For now I think I will just create a function that adds a namespace to my classes

nsClass className =
   class ("someNamespace-" ++ className)

div [ nsClass "label" ] []

But something nicer in the spirit of Html.Styled would be great. But I don't have any ideas.

rtfeldman commented 6 years ago

But something nicer in the spirit of Html.Styled would be great. But I don't have any ideas.

Off the top of my head, here's an idea:

Html.Styled.keyedChildren
    "div"
    [ backgroundColor blue
    , hover 
        [ backgroundColor black
        , childrenWithKey "test"
            [ color white
            ]
        ]
    ]
    []
    [ ( "test", span [] [ text "Hello" ] )
    , ( "", span [] [ text " world!"] )
    ]

This could generate CSS like:

._d74fa3 {
    background-color: blue;
}

._d74fa3:hover {
    background-color: black;
}

._d74fa3:hover > ._d74fa3_test {
    background-color: white;
}

Then it would add class "._d74fa3_test" to the "Hello!" span and nothing to the other element.

However, this approach only works with children, not arbitrary descendants. 🤔

sporto commented 6 years ago

Unsure if this makes any sense, but I imagine something like diving the process into two steps.

First you create the styles, these styles can be used inside other styles to obtain their hash.

labelStyles = 
    Html.Styled.styles [
        color red
    ]

buttonStyles = 
  Html.Styled.styles
   [ backgroundColor blue
    , hover 
        [ class labelStyles.hash
            [ color white
            ]
        ]
    ]

And then they can be used in the Styled.Html

button [ css buttonStyles.hash ] [
    label [ css labelStyles.hash ] [ text "Hello" ]
]
edkv commented 6 years ago

I've tried the approach with storing hover state in the Model and now I think it's actually not that bad. The problem is that it will probably introduce many stateful components that need to maintain their hover/focus/etc state and also to be reusable. And I'm actually okay with this because I'm building my app with components anyway. But for people who want to follow the "reusable views" philosophy this might be an issue.

rtfeldman commented 6 years ago

@sporto Unfortunately that approach will have hash collisions with styles that happen to be identical in the non-hover state.

So if I do this:

labelStyles = 
    Html.Styled.styles [
        color red
    ]

iconStyles = 
    Html.Styled.styles [
        color red
    ]

...and then I do this:

    hover 
        [ class labelStyles.hash
            [ color white
            ]
        , class iconStyles.hash
            [ color black
            ]
        ]

That hover will always set the color to black, and it will do so both for descendants that use iconStyles and for descendants that use labelStyles - because both labelStyles and iconStyles will necessarily have the same hash.

There's no workaround (aside from asking the user for a namespace, i.e. not actually using a hash) because it's impossible to implement a styles function which returns different hashes given identical arguments.

mackdunkan commented 3 years ago

hover [ descendants [typeSelector "..." [color black ]]