Pauan / rust-dominator

Zero-cost ultra-high-performance declarative DOM library using FRP signals for Rust!
MIT License
999 stars 63 forks source link

How to define CSS pseudo classes with ClassBuilder #9

Closed rsaccon closed 5 years ago

rsaccon commented 5 years ago

I am trying to implement a Design System and CSS framework partially based on TailwindCSS with the dominator class!(and stylesheet! if necessary) macros.

Main motivation is to have typed class names and only those CSS classes which are actually used, will end up in the compiled binary (and eventually in the CSSRuleList).

However i got stuck at the following issue: I see no way how I could define pseudo classes with the current ClassBuilder and its class! macro. I would like to do something like:

lazy_static! {
    static ref FOO_CLASS: String = class! {
        .style("background-color", "green")
        .style_hover("background-color", "yellow")
    };
    static ref FOO_CLASS_HOVER: String = class! {
        .base(&*FOO_CLASS)  // not implemented yet. Does it make sense ???
        .pseudo("hover")  // not implemented yet. 
        .style_hover("background-color", "yellow") 
    };
}

That should generate this:

<style>
.__class_0__ {
    background-color: green;
}
.__class_0__:hover {
    background-color: yellow;
}
</style>

Is is currently possible to do something like that ?

Does the code above to implement pseudo classes make sense ? One thing I am not sure about: For the pseudo classes we need the base class name, but when the base class name gets first time accessed (inside the macro for the pseudo class), then maybe the compiler can not optimize it away anymore, in case the base class never gets used in the actual app code.

I am aware that I could build pseudo classes with StylesheetBuilder and its macro, but then I won't have typed class names anymore, which was my main motivation to start with all this.

rsaccon commented 5 years ago

Ups, somehow I confused things when I formulated out this initial issue, it turns out that it is much easier, all what is is needed is a small modification of ClassBuilder to allow something like this:

lazy_static! {
    static ref FOO_CLASS__HOVER: String = class! {
        .pseudo("hover")  // not implemented yet. 
        .style("background-color", "yellow") 
    };
}

That should generate this:

<style>
.__class_0__:hover {
    background-color: yellow;
}
</style>

will try to implement it and provide a PR

rsaccon commented 5 years ago

Ok, I implemented it at my fork and so far it seems to work perfectly. For pseudo classes and elements, the class! macro just has an additional first argument which defines the pseudo class name, see example below.

pub static ref FOO__HOVER: String = class! ("hover", {
    .style("background-color", "yellow")
  });

Unfortunately when I wanted to create a Pull Request, I realized that each file I modified got auto-formatted differently, that makes a PR full of noise, need to sort out that first.

Pauan commented 5 years ago

Hey, sorry for the delay on this (I've spent the past few months moving to a new country). I'll look into this and your other issues soon!

rsaccon commented 5 years ago

No problem, no hurry, thanks anyway and happy country-moving!

I also run into one more issue when using StyleSheetBuilder when trying to do a CSS Reset based on something like normalize.css

when the rule selector is browser specific, e.g.:

button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
  border-style: none;
  padding: 0;
}

then we get a panic, because (I assume) when the currently used browser is not the targeted browser in the rule, a CSS parsing DOM exception gets thrown at insertRule:

I also built a minimalistic wasm-bindgen version of a CssStyleRule injector, just to troubleshoot this issue. If the insertRule fails, I just ignore it, and all seems to be fine. But I am not sure whether this is the right approach, and how to handle this in StyleSheetBuilder. I will further investigate, probably check how the JS libs such as style-mod handle this.

Pauan commented 5 years ago

Is is currently possible to do something like that ?

That's a good question. Currently, it's not possible, but I can add it in.

However, from a philosophical standpoint, CSS was never intended to specify behavior, only appearance.

So the idiomatic way to handle things like hovering is to do this:

let is_hovering = Mutable::new(false);

html!("div", {
    .class(&*FOO_CLASS)
    .class_signal(&*FOO_CLASS_HOVER, is_hovering.signal())

    .event(clone!(is_hovering => move |_: MouseEnterEvent| {
        is_hovering.set_neq(true);
    }))

    .event(move |_: MouseLeaveEvent| {
        is_hovering.set_neq(false);
    })
})

It's easy to wrap that in a mixin:

fn class_on_hover<A>(name: &str) -> impl FnOnce(DomBuilder<A>) -> DomBuilder<A> where A: IEventTarget + IHtmlElement {
    move |dom| {
        let is_hovering = Mutable::new(false);

        dom.class_signal(name, is_hovering.signal())

            .event(clone!(is_hovering => move |_: MouseEnterEvent| {
                is_hovering.set_neq(true);
            }))

            .event(move |_: MouseLeaveEvent| {
                is_hovering.set_neq(false);
            })
    }
}

And now you can use it like this:

html!("div", {
    .class(&*FOO_CLASS)
    .apply(class_on_hover(&*FOO_CLASS_HOVER))
})

One thing I am not sure about: For the pseudo classes we need the base class name, but when the base class name gets first time accessed (inside the macro for the pseudo class), then maybe the compiler can not optimize it away anymore, in case the base class never gets used in the actual app code.

If you use the pseudo class, then it will include the base class as well (this is necessary because the base class generates a unique ID, and it needs to use that ID in the pseudo class).

But if you don't use the base class or the pseudo class, then it won't include either one.

I am aware that I could build pseudo classes with StylesheetBuilder and its macro, but then I won't have typed class names anymore, which was my main motivation to start with all this.

The class names will still be unique, because they're using the base class (which is unique):

lazy_static! {
    static ref FOO_CLASS: String = class! {
        .style("background-color", "green")
    };

    static ref FOO_CLASS_HOVER: String = {
        let name = format!(".{}:hover", &*FOO_CLASS);

        stylesheet!(&name, {
            .style_hover("background-color", "yellow")
        });

        name
    };
}

The above will generate this CSS:

.__class_0__ {
    background-color: green;
}

.__class_0__:hover {
    background-color: yellow;
}

then we get a panic, because (I assume) when the currently used browser is not the targeted browser in the rule, a CSS parsing DOM exception gets thrown at insertRule

Yeah, that seems like a really tricky situation. I'm not sure how to handle that.

Pauan commented 5 years ago

Okay, so I changed the stylesheet macro so it now can accept multiple selectors:

stylesheet!([
    "button::-moz-focus-inner",
    "button::-webkit-focus-inner",
    "button::-ms-focus-inner",
    "button::-o-focus-inner",
    "button::focus-inner",
], {
    .style("background-color", "green")
});

It will try each selector in order, and if a selector errors then it will try the next. As soon as a selector works then it uses it.

I also added in a pseudo! method for class, which lets you specify pseudo elements:

lazy_static! {
    static ref FOO: String = class! {
        .style("background-color", "green")

        .pseudo!(":hover", {
            .style("background-color", "red")
        })

        .pseudo!(":first", {
            .style("background-color", "orange")
        })
    };
}

Similar to stylesheet it also can accept multiple pseudos, which are tried in order until one succeeds:

.pseudo!([
    "::-moz-progress-bar",
    "::-ms-progress-bar",
    "::-o-progress-bar",
    "::-webkit-progress-bar",
    "::progress-bar",
], {
    .style("background-color", "red")
})