yewstack / yew

Rust / Wasm framework for creating reliable and efficient web applications
https://yew.rs
Apache License 2.0
30.91k stars 1.43k forks source link

Styling Brainstorming #533

Closed jkelleyrtp closed 3 years ago

jkelleyrtp commented 5 years ago

I'd like to add a styling system to yew - not having a built-in way of styling components seems a bit unfinished to me. If we want to build better, bigger, and faster web apps, then we need a way of managing styles that doesn't clobber a global namespace and makes sense when looking at the components.

I'd like to see a Stylable trait where we can pull in an existing stylesheet (using css-parser for the servo project), modify it (override the defaults), and then set that as the element style.

We could also have a special style property for each element where we can inject style into even the subdivs of elements.

I personally favor parsing an external stylesheet and doing inline modification because of autocompletes working with .css files, but there is also room for a css! macro and reserve a keyword for the element that the style is being applied to, to access subdivs in that element.

Curious what the thoughts of the community is before I try pull my changes into the project.

ThomasdenH commented 5 years ago

I think the system for styling would be best as a separate system, where individual properties are ideally strictly typed. Then the library could be integrated here.

jstarry commented 5 years ago

I agree with @ThomasdenH and think this could start out as a separate crate but could eventually get added to yew later as it matures, perhaps in the form of "styled components".

@jkelleyrtp are you thinking of taking approach like https://cssinjs.org/? I would love having a way to use declarative styles like this. Then the integration with yew is as simple as setting the class name

jkelleyrtp commented 5 years ago

@jstarry I was definitely feeling inline CSS in the same way that JSS works - I like having the state of the component directly tied to its appearance. I'm going to approach this in the same way that styled-components work so we can just drop existing stylesheets into components but also be able to modify them on component updates.

matiu2 commented 5 years ago

Personally I just use bootstrap. Then apply the class attributes as needed: https://hackerthemes.com/bootstrap-cheatsheet/

I don't use the JS part of it; just the stylesheets.

I can imagine possibly a high level API that just applies those, but I think I would prefer to just apply them myself.

The question is where does the scope of yew end ? perhaps what's needed is a separate yew-styling plugin/library ?

hgzimmerman commented 5 years ago

@jkelleyrtp Have you made any progress towards this? I have a loosely defined proposal on how to implement this typed up and would love to give it a shot in building it, but if you have something in the works, I would rather hold off and wait on that.

hgzimmerman commented 5 years ago

Nevermind, I've got a prototype in the works here in case anyone wants to check it out: https://github.com/hgzimmerman/yew_css

MuhannadAlrusayni commented 4 years ago

Here is my thoughts on building styling system.

I think there are two different places that we need to handle style:

  1. a default style for every component.
  2. a unique style for every component instate.

A default style for every component

Every component can have it's own default style, let's say a Button component have this default style:

color: black;
background: white;
border: 1px solid black;
border-radius: 4px;

At this point we need to defined the behavior of the styling system, thus we need to answer these questions:

A unique style for every component instate

Some times we change style for component instate, by adding, removing .. etc styles to the component instate, thus we need to defined a how the styling system would allow us to do that.

possible answer for the 1st question

How the styling system will get these default styles? and from where? By using trait Theme that would return Style struct that contains CssName and CssValue pairs, for the component. Every theme struct would implement this trait.

Theme that uses classes

This trait uses the class to construct Style for it. this trait is simple to implement but doesn't have full access to the component state. it also can have styles even if we have no components for them, it is easy to reuse existing css libraries with this trait.

    pub trait Theme {
        fn style(&self, class: impl Into<Cow<'static, str>>) -> Option<Style>;
    }

    // example
    pub struct DefaultTheme;

    impl Theme for DefaultTheme {
        fn style(&self, class: impl Into<Cow<'static, str>>) -> Option<Style> {
            match class.into().as_ref() {
                "button" => unimplemented!(),
                "flexbox" => unimplemented!(),
            }
        }
    }

Theme uses components state

This trait uses the component state to build Style for it, it have full access to the component state, but it's not simple to implement as well as it's only support components that are present in Component enum.

    pub trait Theme<Comps> {
        fn style(&self, class: impl Into<Comps>) -> Option<Style>;
    }

    // example
    pub struct DefaultTheme;

    impl<'a> Theme<Components<'a>> for DefaultTheme {
        fn style(&self, comp: impl Into<Components<'a>>) -> Option<Style> {
            match comp.into() {
                Components::Button(btn) => unimplemented!(),
                Components::Flexbox(flexbox) => unimplemented!(),
            }
        }
    }

    pub enum Components<'a> {
        Button(&'a Button),
        Flexbox(&'a Flexbox),
    }

This my try, for sure there are a lot of things that need to be changed for these ideas to fit in Yew. I didn't answer the other questions, since I don't have clear answer for them.

richard-uk1 commented 4 years ago

I'm having a play with writing a css parser from rust tokens, and it seems to be going well. It's available at https://github.com/derekdreery/style. The idea is that you can do things like

<div style={{font-size: 10px; flex-direction: column}}></div>

from within a macro in rust code, making using css/styles feel really natural.

jstarry commented 4 years ago

Wow, awesome @derekdreery! Looks like the yew html macro could depend on your style macros under the hood. Is that what you had in mind?

kellytk commented 4 years ago

I agree with

individual properties are ideally strictly typed

from https://github.com/yewstack/yew/issues/533#issuecomment-513581931.

richard-uk1 commented 4 years ago

Looks like the yew html macro could depend on your style macros under the hood. Is that what you had in mind?

Happy to help out however you want to use them.

There are a couple of small caveats:

individual properties are ideally strictly typed

The lib I'm working on does typecheck your CSS, so only valid types of property values are allowed for a given property name.

fromrileywithlove commented 4 years ago

My preference is to use css modules, or css that's localised to the component, keeping components from conflicting with each other automatically. To this end there is the css-modules crate, which simply aliases all of the class names in a css file and imports a list of those aliases into rust.

The author of the crate was recently harassed into deleting their GitHub account by a group of transphobes but everything still works just fine.

Edit: I forgot to say that I also use it with rollup for post processing.

bl42 commented 4 years ago

First off, New to rust & New to yew... Figure it would be a great place to learn both!

Recently been using a lot of Svelte and I have really enjoyed their take on styling.

<style>
    p {
        color: purple;
        font-family: 'Comic Sans MS', cursive;
        font-size: 2em;
    }
</style>

<p>Styled!</p>

They do stuff under the hood of generating classes and attaching them to the elements but it avoids concepts of theming and leaves it to the developer/css.

Ideally (for me) an API like so would exist like so where I could leverage css variables to do the theming/customization of a child component in CSS.

impl Component for Model {
  fn style(&self) -> Css {
        css! {
            div {
                background: black;
                --main-text-color: white;
            }
        }
    }
    fn view(&self) -> Html {
        html! {
             <div>
                  <MyComponent /> <!-- changes child's --main-text-color by setting the var -->
             </div>
         }
     }

It may not be possible (still learning) but I would be interested if it does.

jstarry commented 4 years ago

@bl42 I like the look of that.

Minor detail: the function signature wouldn't quite work like that if the CSS was generated at compile time since we wouldn't have an instance of the component then.

Ideally (for me) an API like so would exist like so where I could leverage css variables to do the theming/customization of a child component in CSS.

I don't quite understand your vision here, could you elaborate a bit more?

MuhannadAlrusayni commented 4 years ago

I have developed trait with similar functionality, Component should declare their Style struct that will be used to style the component (e.g. Entry) instead of using some sort of collection that doesn't help at checking the used style if it works with the Component or not.

My project using Seed, but I'm pretty sure this idea is applicable in Yew too.

bl42 commented 4 years ago

I don't quite understand your vision here, could you elaborate a bit more?

My preferred output would have some "Global Styles" that I can declare for the app (on mount)

Something like

   @import 'google fonts...'
   @import 'css animations lib...'

   html {
      font-size: 65.25%
      --primary-color: #000;
      --primary-text: #fff;
      etc..
   }

and in each component have the ability to call color: var(--primary-text);

We could easily store these values in Rust... but I suggest to let CSS do what it is good at.

lukidoescode commented 4 years ago

Hi I just published a library that is supposed to help with that. I was in need for a framework that works in a way that styled components work. Here is what I came up with: CSSinRust https://crates.io/crates/css-in-rust. Feel free to open Issues and/or PR's. The target is to make it work with any components framework.

icyJoseph commented 4 years ago

Since this PR's title is brainstorming. I'll throw in my 5 cents with Components as a way to hide style implementation details.

Libraries like Styled Components enable developers to bind style at a Component level, which leads to the removal of the class to node mapping. It also enforces more declarative JSX over prop controls, when done correctly.

In JS, template literals give the ability to evaluate the Component's CSS declaration (a string mixed with expressions) through the components own props. I guess in rust a macro would do this instead.

These styles are added to the <head> of the document as <style> tags, enabling any CSS capabilities you'd like.

I disagree with having defaults for styles, because the browser already takes care of that. One should also consider, how could I reset, or normalize, or simply set some global properties on my stylesheet? What about prefixes? Perhaps this is out of the scope of Yew.

richard-uk1 commented 4 years ago

@icyJoseph this sounds good. It would be nice to typecheck the styles at compiletime then generate the appropriate string templates for use at runtime.

JamesPatrickGill commented 4 years ago

Similar to @icyJoseph I'm just throwing my ideas at the wall here, but as a web developer my experience is that developers are embracing tools such as styled components as it means everything is "just JS" - meaning one can handle dynamic prop based styles intuitively whilst the clear component scoping reduces the classic "why has my div flew half down the screen I only want the title to be red".

I believe the "just JS" approach to styling in yew ( "just rust" ) and building a rust-y implementation of "styled-components"-ish style system thats fits the needs of yew could lead to not only a more consistent developer experience due to projects being only one language but also gives oppurtunity to build more advanced tooling around styles be that in the editor or oppurtunity for minimising style size at build. The things styled components does well and has brought to the ecosystem, are almost dying to implemented in the rust.

I would also like to say I would like to keep an eye on this and am happy to discuss further and get involved in any proof of concepts. Not sure the etiquette here but if I wanted to start work on this to see how it would work, would I need for this discussion to be resolved or would I need to create some code and build a discussion around a PR. Thanks for any pointers.

emmanuelantony2000 commented 4 years ago

What if we use something like Rust structs and enums, something like:

style = Style {
    BackgroundColour(Colour::Black),
    Colour(Colour::RGB(140,140,140),
    .. Style::default()
}

Rather than going for parsing or macros, I feel this might be better.

strwrite commented 4 years ago

I like the way material UI supported styles (it's CSS framework for react): https://material-ui.com/styles/basics/

They are using type definitions for css properties from https://github.com/frenic/csstype (it's for typescript) and use it in functions like createStyles, where user defines named classes (similar to css classes) and then can use those named classes later in tsx template.

They also allow theming, when createStyles call wrapped to makeStyles, which receive (contextual) theme.

I think it's a great example and it would be cool to have something similar for yew. The most complicated part in this is (I think) typing all of css properties in Rust types, but that work worth it.

mishaszu commented 4 years ago

I like the idea behind StyledComponents but in practice, it's just pure pain.

  1. It creates unnecessary wrappers in DOM structure
  2. Debugging node that nests few elements that extends the same ancestor is a nightmare
  3. Bad performance for complex structures (like data grids).

The nicest approach to CSS I saw so far is bs-css that is Statically typed DSL for writing css in reasonML

It's similar to what @emmanuelantony2000 proposed. Instead of enums reasonML have variadic types and polymorphic variant types (sure it's not the same as Enums, I'm not saying it can be easily replicated in Rust). For example:

  open Css;
  let card = style([
    display(flexBox),
    flexDirection(column),
    alignItems(stretch),
    backgroundColor(white),
    boxShadow(Shadow.box(~y=px(3), ~blur=px(5), rgba(0, 0, 0, 0.3))),
    unsafe("-webkit-overflow-scrolling", "touch"),
    padding(Theme.basePadding)
  ]);

(all definitions are imported by opening Css module) And then it can be easily reused as a class:

<div className=card></div>

The only bs-css cons I found so far is that classes names are generated quite randomly so it's still hard to navigate through HTML elements. Besides that typed system for CSS is super satisfying to write stylings.

richard-uk1 commented 4 years ago

^ related prior art: https://github.com/purescript-contrib/purescript-css

not-a-bobbit-fish commented 4 years ago

A couple of observations:

  1. I agree that styled-components tries to do something that doesn't, in the end, work. Always when using styled-components, the best course of action is just to create classes using the css`` tagged string and use those, because styled tags cannot have their stylings merged into other styled tags, and you will need to separate those out if you ever want to spread them into new derived styles.
  2. In CSS style declarations, order matters, and duplicate values are not exactly overwritten. For example, cursor: pointer; cursor: hand; was an old idiom for cross-browser cursor support. The complexity here would best be handled by offloading to an existing library like libsass during compilation, and unfortunately I think this means that plain Rust structs (per @emmanuelantony2000 's comment) would not be able to represent the full range of CSS someone could write. We could of course still support Rust structs as well, but it wouldn't cover the full gamut if you will.
  3. Sometimes we would want parameterized classes, for having a base template that can be overridden with certain values to plug in to one or two styles.
  4. I would argue that a good V1 of this would restrict class declarations to being done exclusively at build time. That way, for features like SSR or even just CSS minification, all the app's CSS is known at build time and can be collected into a common location before being packed for distribution. It also makes easier the problem of coordinating a hash set to ensure CSS classes don't have overlapping names.
philip-peterson commented 4 years ago

Just saw this on my feed today, a Sass compiler built in pure Rust: https://github.com/connorskees/grass

lukidoescode commented 4 years ago

CSSinRust is now compatible with yew 0.17.* (https://crates.io/crates/css-in-rust). Check out https://github.com/lukidoescode/yew-fullstack-boilerplate on how to use it. I'd appreciate any PRs.

onehundredfeet commented 4 years ago

Just saw this on my feed today, a Sass compiler built in pure Rust: https://github.com/connorskees/grass

This looks great. Is there a way of providing a sass or css file that overrides these defaults? It would be great to give to an artist for them to tweak.

lukechu10 commented 3 years ago

How about the way vueJS does it: https://vue-loader.vuejs.org/guide/scoped-css.html#deep-selectors

You could add an unique id to every component, modify the css which should be pretty simple, and recursively add an attribute to every DOM element in the component, perhaps at render time.

edkv commented 3 years ago

My dream is to have a component-oriented solution like Yew, but to be able to use some custom styling/layout language instead of HTML/CSS, something like iced has, or like elm-ui. The biggest issue I see with CSS is that styles are separated from HTML and the view is splitted into two parts as a result. You can also take a look at elm-css that solves this by allowing you to embed CSS directly into HTML and then auto-generates classes from that, but I don't believe that's the right direction. I'm convinced that the better solution is to build abstractions over HTML/CSS instead (that maybe could then even compile to other platforms like in the case of iced).

I haven't tried Yew yet actually, just trying to contribute to the discussion by bringing some ideas :)

teymour-aldridge commented 3 years ago

I have one possible solution: https://github.com/lovelace-ed/lovelace/blob/main/utils/mercutio/src/lib.rs

Note that it depends on Malvolio (another crate I've written) and I can't publish the next version of that until Rocket cuts its next release 🤷🏼

fxdave commented 3 years ago

Reading this conversation, the bs-css was the best implementation example, I think.

One more thing to consider is that the browser downloads the whole wasm code at once. As far as I know, yew cannot split bundles currently. So maybe it's not the best performance to keep the CSS in rust right now. Also, CSS in WASM requires two compilations, one for WASM, one for CSS. As I see, currently the css-modules is the best performing option with in-rust syntax checking.

EndilWayfare commented 3 years ago

I think I'm approaching the "styling" question from a slightly different angle. "CSS-in-<language>" may be a good fit for modular aesthetic/layout styling, but it seems a bit heavy-handed for data-driven styles where object-identity of an element/component matters more than its semantic-identity.

For example, I'm currently working on a Resizable component (vastly simplified MVP based on re-resizable) that is most simply implemented in React with the style prop. Creating a new style tag (or diffing into one) seems like a lot of overhead to just "update width/height of this specific element in response to mouse/touch event shenanigans". Inline, element-level styles can easily be abused, I'm not disputing that. But I think they still have a place.

The obvious workaround is to use NodeRefs and mutate styles in update (or use js-sys/external .js to wire events directly, using yew only for bootstrapping). This is tempting, but you give up the advantages of pure unidirectional flow. You also have to special-case your initial render, because your NodeRefs don't exist yet. You may also have to do a lot of bookkeeping if elements-with-styles can pop in/out of existence based on Props (e.g. if "what edges/corners are draggable" can change dynamically).

Handling a style prop along the lines of the current html! macro system sounds like a bit of a nightmare. Striking a balance between static verification, ergonomics, and performance seems seriously nontrivial. Maybe it's not worth it to support use-cases like mine? But, just wanted to throw it out there, because it seems like an angle otherwise missing from the discussion.

srid commented 3 years ago

Composable styles are becoming popular, via https://tailwindcss.com/

Today I can use the twind shim in a Yew app and get Tailwind support for free, but a proper CSS compiler that generates only the classes in use inside the html! macro (like postcss does in the JS world) woud be nice. See also https://windicss.org/ for some ideas (it compiles and replaces the N-classes into a single class).

ekanna commented 3 years ago

Tailwindcss JIT compiler https://tailwindcss.com/docs/just-in-time-mode

futursolo commented 3 years ago

Since there have been no commits on the css-in-rust crate for over a year, I have decided to create a fork to provide Yew 0.18 support and fix a couple known issues that I had when using the crate as well as some improvement on the internals.

https://github.com/futursolo/stylist-rs

I would appreciate any feedback on this.