Pauan / rust-dominator

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

import and encapsulate CSS #36

Closed dakom closed 4 years ago

dakom commented 4 years ago

This has come up in a couple other issues, like https://github.com/Pauan/rust-dominator/issues/35, but I think it deserves its kindof its own issue too...

Is there a way to import .css files?

How about importing them so that they are encapsulated within a particular component/tree? (shadowDOM or not)?

How about doing that, but also being able to import common files (like "theme-dark.css", "base.css", etc.) - without having it add bloat at each additional import?

dakom commented 4 years ago

Also - not quite clear on what the class! macro is or how it works. Some explanation would be great!

Pauan commented 4 years ago

Yeah of course, you just import them the same way that you import them in HTML:

html!("link", {
    .attribute("rel", "stylesheet")
    .attribute("href", "/path/to/foo.css")
})

How about doing that, but also being able to import common files (like "theme-dark.css", "base.css", etc.) - without having it add bloat at each additional import?

I'm not sure what you mean, but you can use @import url("base.css") within your .css files to import other .css files.

Also - not quite clear on what the class! macro is or how it works. Some explanation would be great!

The class! macro creates a CSS stylesheet, adds a CSS rule into it, and then adds CSS styles to that rule. When you do this...

lazy_static! {
    static ref FOO: String = class! {
        .style("background-color", "red")
        .style("width", "500px")
        .style("height", "500px")
    };
}

...that is exactly the same as doing this in HTML:

<style>
.__class_1__ {
    background-color: red;
    width: 500px;
    height: 500px;
}
</style>

The FOO static contains the string "__class_1__", so when you use .class(&*FOO) in dominator, that just adds the __class_1__ class to the DOM node.

So it's just a simple way of injecting CSS stylesheets into the page (with a unique classname), and then adding that classname to DOM nodes.


If you're curious how it actually does this:

  1. It calls document.createElement("style").

  2. It adds the <style> to the <head>.

  3. It uses the sheet property to access the CSSStyleSheet for the <style> element.

  4. It creates a unique classname, and then inserts a CSS rule. It does this by using the cssRules and insertRule APIs.

  5. It now has a CSSStyleDeclaration (which is the same as the .style property on DOM nodes, except it modifies the <style>).

    So it can now set CSS properties on that CSSStyleDeclaration (in the same way that it sets CSS properties on the .style of DOM nodes).

Basically, if I translated dominator's code to JS, it would look like this:

let counter = 0;

function makeClassId() {
    ++counter;
    return "__class_" + counter + "__";
}

function makeStylesheet() {
    const e = document.createElement("style");
    e.type = "text/css";
    document.head.appendChild(e);
    return e.sheet;
}

function makeStyleRule(sheet, selector) {
    const rules = sheet.cssRules;
    const length = rules.length;
    sheet.insertRule(selector + "{}", length);
    return rules[length];
}

function makeClass(f) {
    const className = makeClassId();

    const sheet = makeStylesheet();
    const rule = makeStyleRule(sheet, "." + className);

    f(rule.style);

    return className;
}
const FOO = makeClass((style) => {
    style.setProperty("background-color", "red");
    style.setProperty("width", "500px");
    style.setProperty("height", "500px");
});

document.body.className = FOO;
dakom commented 4 years ago

Ahh... thanks... seems it answers the encapsulation question too, since one can either use the link approach within the shadowDOM, or use the dynamic class name approach anywhere.

From my perspective - feel free to close!

Pauan commented 4 years ago

Once constructable stylesheets are available, it will become a lot easier to create and inject stylesheets into the DOM (including within the shadow DOM), but for now creating <link> and <style> within the shadow DOM is your best option.

MendyBerger commented 1 year ago

Any plans to do constructable stylesheets in the near future? They're now available in Chrome and in Firefox

Pauan commented 1 year ago

They're still not available in Safari, they only have 61% availability.

MendyBerger commented 1 year ago

Is the ability to use it with a polyfill not enough?

Pauan commented 1 year ago

There aren't any correct polyfills, because this feature can't actually be polyfilled. For example, this polyfill has various limitations and issues. Do you need this feature?

MendyBerger commented 1 year ago

I do need it. In our current setup we use Lit to create custom elements and Dominator to render those elements from Rust. We need Lit since Dominator doesn't have components where you can scope styles. I think that constructable stylesheets combined with .shadow_root! could solve that need for our codebase and help us get rid of Lit and custom elements.

This might be a long shot, how about adding and actual style tag for browsers that don't support constructable stylesheets? This is the way Lit does it and it works quite well.

Pauan commented 1 year ago

Oh that's a different issue, I already had plans to support a dynamic class! which can be inserted/removed from the DOM (including shadow roots). That can be easily done without adoptedStyleSheets.

You can currently do that right now by the way:

html!("div", {
    .shadow_root!(ShadowRootMode::Closed => {
        .child(html!("style", {
            .text("...")
        }))

        .child(...)
    })
})
MendyBerger commented 1 year ago

This is great! But I still need adoptedStyleSheets for browsers that do support it, any way to do that?

:+1: for dynamic classes

Pauan commented 1 year ago

Why do you need that?

Since you have full access to the DOM, there's nothing stopping you from doing that:

#[wasm_bindgen(inline_js = "
    export function set_stylesheets(node, sheets) {
        node.adoptedStyleSheets = sheets;
    }

    export function new_stylesheet() {
        return new CSSStyleSheet();
    }
")]
extern "C" {
    fn set_stylesheets(node: &Node, sheets: &Array);
    fn new_stylesheet() -> CssStyleSheet;
}

html!("div", {
    .shadow_root!(ShadowRootMode::Closed => {
        .before_inserted(|node| {
            let style = new_stylesheet();

            // Set styles on the stylesheet...

            set_stylesheets(&node, &[style].into_iter().collect::<Array>());
        })

        .child(...)
    })
})
MendyBerger commented 1 year ago

I need that so that I can have the same component multiple times on the same page without ending up with lots of the same stylesheets.

Haven't thought about doing it manually, sounds like a great option! Thanks!

Pauan commented 1 year ago

It will be a bit tricky to figure out the best API for this, which is why I haven't implemented it earlier... I'm thinking something like this seems reasonable:

static CLASS: Lazy<ClassBuilder> = Lazy::new(|| class_builder! {
    .style("width", "100px")
    .style("margin", "5px")
    .style("background-color", "red")
});

html!("div", {
    .shadow_root!(web_sys::ShadowRootMode::Closed => {
        .stylesheets([
            CLASS,
        ])

        .child(html!("div", {
            .class(&*CLASS)
        }))
    })
})

This should be future-compatible with constructable stylesheets.

MendyBerger commented 1 year ago

Any advantage of using .style rather then just accepting a CSS string? It's not like the CSS in .style is strongly typed

Pauan commented 1 year ago

Yes, it is consistent with DomBuilder, so you can easily copy-paste between them. And it also supports .style_signal, .style_important, and .style_unchecked.

It also makes it very easy to use dynamically computed values, for example:

https://github.com/Pauan/tab-organizer/blob/f97823184ec02f1b1b23eee269baa7d621b43139/src/sidebar/src/constants.rs#L66-L72

Also, .style is checked at runtime to verify that it's correct. That wouldn't be possible if it was a single giant string.

dominator consistently uses the same syntax for class!, html!, and stylesheet!, it would be unusual to change the syntax just for the Shadow DOM.

MendyBerger commented 1 year ago

About consistency: what is the general advantage of using .style in class!/html!/stylesheet!? Why not use strings everywhere? Is it just for .style_signal?

Pauan commented 1 year ago

The same reasons I said above.