seed-rs / seed

A Rust framework for creating web apps
MIT License
3.8k stars 153 forks source link

Final element macros API #525

Open MartinKavik opened 4 years ago

MartinKavik commented 4 years ago

Previous work:

Let's design the final element API so we can stabilize it.

The first draft / a foundation for brainstorming:

fn view(model: &Model) -> Node<Msg> {
    let disabled = true;

    custom![
        TAG.custom("custom-element"),
        ID!("my-container"),
        C!["container", "my-class"],
        AT.id("my-container").class(C!["container", "my-class"]),
        AT
            .id("my-container")
            .class(C!["container", "my-class"]),
        CSS
            .position(CssPosition::Relative)
            .flex_grow(1)
            .w(CssWidth::Auto),
            .my(0)
            .mx(CSSMargin::Auto)
        CSS
            .only(Breakpoint::Desktop)
            .max_w(px(960)),
        CSS
            .only(Breakpoint::WideScreen)
            .max_w(px(1152)),
        AT 
            .id("my-container")
            .class(C!["container", "my-class"])
            .class("container my-class")
            .disabled(disabled)
            .autocomplete(AtAutocomplete::On)
            .autofocus(true)
            .custom("data-columns", "3")
            .custom("custom-boolean", true)
            .width(300)
        EV
            .click(|event: EvMouse| { event.prevent_default(); Msg::Clicked }),
            .click(|_| log!("Clicked!")),
            .custom("button-clicked", |event: EvCustom| ())
            .input(|event: EvInput| Msg::OnInput(event.value())
            .input(|event| Msg::OnInput(event.value())
            .input(Msg::OnInput),
        button![...],
        div![...],
        button![...],
    ]
}
TatriX commented 4 years ago

After writing tones of markup using old and new macros my subjective conclusions is that I really dislike uppercase and shortened macros. So I strongly prefer class![] over C![] and when!() over if cond { } else { } over IF!().

I would prefer module style syntax over javascripty version:

tag::custom("custom-element")
// instead of 
TAG.custom("custom-element"),

IIRC main reason to go for C![] over class! was that it's easy to confuse the later for with a tag. I personally never had that problem but I understand the logic. So perhaps we can solve this by using module for non-tag macros:

div![
    attr::class!("center"),
    attr::id!("app"),
]

In this case one can add use attr::class; and use it like it used to be: div![class!["foo"]].

Instead of chaining I would prefer having a list of arguments. It makes moving, adding and removing easier and symmetrical:

attr::list(&[
    attr::id("my-container"),
    attr::classlist(attr::class!["container", "my-class"]),
    attr::class("container my-class"),
    attr::disabled(disabled),
    attr::autocomplete(attr::Autocomplete::On),
    attr::autofocus(true),
    attr::custom("data-columns", "3"),
    attr::custom("custom-boolean", true),
    attr::width(300),
]),

// or in case you really add that many attrs often:
fn complex_container<T>() -> Node<T> {
    use seed::attr::{self, *}; // or maybe `attr::prelude::*` with favorite attrs

    div![
        attr::list(&[
            id("my-container"),
            class!["container", "my-class"],
            attr::class("container my-class"),
            disabled(business_logic_check(&model.data)),
            autocomplete(Autocomplete::On),
            autofocus(true),
            custom("data-columns", "3"),
            custom("custom-boolean", true),
            width(300),
        ])
    ]
}

Note, I'm using attr instead of at here, because that's more natural for me to read and write to.

flosse commented 4 years ago

After writing tones of markup using old and new macros my subjective conclusions is that I really dislike uppercase and shortened macros. So I strongly prefer class![] over C![] and when!() over if cond { } else { } over IF!().

I agree :+1:

TatriX commented 4 years ago

Here's a quick and dirty demo of how I would like typed css to look like:

fn main() {
    let s = attr::style(&[
        css::position(css::Position::Relative),
        css::flex_grow(1),
        css::width(css::Auto),
        css::width((0, css::Auto)),
    ]);
    println!("{}", s);
    println!("------");
    println!("{}", attr::style(&button_style()));
}

fn button_style() -> [css::Property; 4] {
    use css::*;
    [
        position(Position::Relative),
        flex_grow(1),
        width(Auto),
        width((px(10), Auto)),
    ]
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f5beb1dc254190844dc2879252d99f75

MartinKavik commented 4 years ago

Another draft based on @TatriX's examples:

fn view(model: &Model) -> Node<Msg> {
    let (disabled, active, blue) = (true, true, true);

    custom![
        tag::custom("custom-element"),
        attr::id("my-container"),
        attr::class!["container", "my-class", when!(active => "active")],
        list![
            css::position(css::Position::Relative),
            css::flex_grow(1),
            css::w(css::Auto),
            css::my(0),
            css::mx(css::Auto),
            css::group![
                css::only(Breakpoint::Desktop),
                css::max_w(px(960)),
            ],
            css::group![
                css::only(Breakpoint::Desktop),
                css::max_w(px(1152)),
            ],
        ],
        list![ 
            attr::id("my-container"),
            when!(blue => attr::class!["container-blue", "my-class-blue"]),
            attr::class("container my-class"),
            attr::disabled(disabled),
            attr::autocomplete(attr::Autocomplete::On),
            attr::autofocus(true),
            attr::custom("data-columns", "3"),
            attr::custom("custom-boolean", true),
            attr::width(300),
        ],
        list![
            ev::click(|event: ev::Mouse| { event.prevent_default(); Msg::Clicked }),
            ev::click(|_| log!("Clicked!")),
            ev::custom("button-clicked", |event: ev::Custom| ()),
            ev::input(|event: ev::Input| Msg::OnInput(event.value()),
            ev::input(|event| Msg::OnInput(event.value()),
            ev::input(Msg::OnInput),
        ],
        div!["Custom Elements"],
    ]
}
TatriX commented 4 years ago
css::w(css::Auto),
css::my(0),
css::mx(css::Auto),

What is w, my and mx? I have a guess, but I would argue that we shouldn't invent alternative parlance for styles and stick with css way of doing things.

css::group![
    css::only(Breakpoint::Desktop),
    css::max_w(px(960)),
],

What is group and what semantic does it have? If it's for doing media queries, should we call them exactly that then:

css::media::query(css::media::min_width(px(300)), &[
  css::max_width(px(960)),
])

Then I guess we can optionally build breakpoints as batteries included on top of that.

MartinKavik commented 4 years ago

css/CSS API in my drafts is based on @rebo's Style. Style's API is still changing according to feedbacks but it already works great. I would like to integrate it into Seed - it means that element macros designed in this issue significantly affects also Style. So I would like to wait for @rebo's feedback and opinions to move forward.

Some Style references:

OYangXiao commented 4 years ago
css::w(css::Auto),
css::my(0),
css::mx(css::Auto),

What is w, my and mx? I have a guess, but I would argue that we shouldn't invent alternative parlance for styles and stick with css way of doing things.

css::group![
    css::only(Breakpoint::Desktop),
    css::max_w(px(960)),
],

What is group and what semantic does it have? If it's for doing media queries, should we call them exactly that then:

css::media::query(css::media::min_width(px(300)), &[
  css::max_width(px(960)),
])

Then I guess we can optionally build breakpoints as batteries included on top of that.

The w my wx things makes me feel like something actually is CSS but with changed names, I mean, do we have to rename css properties... again? Especially when there are frameworks like tailwind and more?

TatriX commented 4 years ago

My take on this would be to use names exactly like in css, so I can copy styles from devtools replacing - with _ and not have additional cognitive load on mapping names. Same applies for multiple argument values:

attr::style![
   css::margin(0),
   css::margin((px(10), px(20)),
   css::margin((px(10), px(20), px(10)),
   css::margin((px(25), em(20), vh(10), "20%"),
]

There is already a scheme people know so there should be a very good reason to make them learn another one. @rebo could you please tell us what you think?

MartinKavik commented 4 years ago

The w my wx things makes me feel like something actually is CSS but with changed names, I mean, do we have to rename css properties... again? Especially when there are frameworks like tailwind and more?

Style / css should replace libraries like Tailwind. Also these names are only alternatives - i.e. you can write .w or .width. There is a reason why libraries like Tailwind have been developed - CSS is hard and API/names are often a little bit weird or unpractical. You can ask why we have div and p instead of divider and paragraph in HTML - it's just more practical if you know them, however I'm sure some people would prefer the longer names.


attr::style![
   css::margin(0),
   css::margin((px(10), px(20)),
   css::margin((px(10), px(20), px(10)),
   css::margin((px(25), em(20), vh(10), "20%"),
]

There is already a scheme people know so there should be a very good reason to make them learn another one.

I still can't remember what each value means in margin - it means additional cognitive load for me and searching through docs. If something is known doesn't automatically mean that it's the best way. In ideal world we won't need to use CSS, HTML, DOM and browser JS API but when we have to use it, let's make it at least less painful.


@rebo could you please tell us what you think?

rebo is quite busy now, but he knows about the issue.

nielsle commented 4 years ago

Right now It is easier to search though the official CSS docs than it is to search through the Seed docs. So it would be great to be able to translate the CSS directly into Seed-CSS without having to learn extra abbreviations. This would lower the barrier of entry.

BTW: I wonder if rust-analyzer can expand function names within seed macros. Perhaps it isn't so bad to have to write long names.

TatriX commented 4 years ago

I still can't remember what each value means in margin

It's quite simple to understand and thus remember.

I don't buy Tailwind argument. Frameworks change every now and then, but css here to stay. I prefer my foundational web framework to be less opinionated, so I can add custom css or fetch library on top.

asvln commented 4 years ago

CSS is hard and API/names are often a little bit weird or unpractical.

There is no denying CSS is terrible in so many ways, but it is what we are outputting. There is complexity, but most of it has been standardized for decades and over the course of that time, very little has been deprecated/changed.

I still can't remember what each value means in margin - it means additional cognitive load for me and searching through docs.

There are very few abstractions used in CSS, and to take them away would confuse so many newcomers who already have experience using CSS, possibly years of experience (the majority of people who are going to consider using Seed). One of the first things I did when adding CSS to a project was to use St::Margin => "0 auto".

You can ask why we have div and p instead of divider and paragraph in HTML

Nested tags are very different than identifiers. It is easier to visually grep short nested tags and much easier to read slightly more verbose stacked identifiers:

w(Auto),
my(0),
mx(Auto),

...takes longer for me to parse than...

width(Auto),
margin((0, Auto)),

even though the former is faster to type.

If something is known doesn't automatically mean that it's the best way. In ideal world we won't need to use CSS, HTML, DOM and browser JS API but when we have to use it, let's make it at least less painful.

Are we trying to make CSS less painful or faster to type?

The DOM and browser JS APIs are complex operational things that most people would not want to touch with a 20ft coding pole. These need to be abstracted away.

There is very little complexity lurking in CSS beyond it's inherit flaws in how it is designed. Once we try to fix the flaws in the design, we are creating a brand new styling system which we have to translate into CSS. However, this isn't even what is going on with this Tailwind system for the most part. It seems to be specifically for the use case of placing faux-raw CSS quickly into raw HTML.


There is a reason that SASS became by far the most used CSS framework. It's because it is regular CSS...

  1. Which most people already understand.
  2. Is what actually makes up webpage and isn't changing anytime soon.

...but with the ability to abstract as the designer sees fit.

We already have this ability because we are writing everything in Rust. We should use that to our advantage by not abstracting something which then makes it more complicated to personally abstract.


Style / css should replace libraries like Tailwind.

I am for the viewpoint that:

Style / css = CSS

and

seed { feature = "tailwind-css"} or seed-tailwind = 0.8.0

If we bring Tailwind into Seed, why not abstract away HTML also and make Savory a core component?

Even by altering the way we approach horizontal and vertical assignments for margin, how many more steps until we are at this point... (taken from tailwindcss.com)

<div class="max-w-sm mx-auto flex p-6 bg-white rounded-lg shadow-xl">
  <div class="flex-shrink-0">
    <img class="h-12 w-12" src="/img/logo.svg" alt="ChitChat Logo">
  </div>
  <div class="ml-6 pt-1">
    <h4 class="text-xl text-gray-900 leading-tight">ChitChat</h4>
    <p class="text-base text-gray-600 leading-normal">You have a new message!</p>
  </div>
</div>

I know CSS very well and I have no idea what is going on here and I don't want to.

For me (and I think most web designers), that is an excessive additional cognitive load.

arn-the-long-beard commented 4 years ago

Hello guys

I have my personal challenge with css and as @asvln states :

There is no denying CSS is terrible in so many ways, but it is what we are outputting.

What is w, my and mx ? I have a guess, but I would argue that we shouldn't invent alternative parlance for styles and stick with css way of doing things.

I do agree with my teammates. There are few points I think we can resume here :

- 1 Seed is easy & accessible :100:

The huge advantage of Seed over Angular+Ngrx or Vue, or React is that we have minimal "Framework" vocabulary and way of doing stuff. Seed is really easy, sooo accessible and productivity can be reached faster because of its transparency against standard html/css.

We should not lose this advantage.

- 2 Seed respects standard html css ( or tries to ) and make us better programmers :brain:

Personally, having to do pure html ( through Rust macro of course ) and css as normal or with macro made me a better web programmer instead of just using already made material/angular component or specific framework concept. I got to know better the standards Api stuff now. Growing new people to web with Seed is a very powerful strategy that many teams and companies will take once Seed is known and ready for production also.

Also that is the goal of Rust to make us better programmers. So Seed fits into the programming language objective as well.

We should not lose this cultural advantage.

- 3 Seed project is about building an awesome front end framework :trophy:

The job of Seed is to make us using Rust to do web stuff as normal. It means, replacing JavaScript with Rust, doing Html and css as usual or through our macro.

It is out of the spec of Seed to change the css standard. That is why I strongly believe that we should not change the css Apis & semantic through Seed. Because this is not the focus I think. It is not our job.

We should stay focused on our core business.

I like Seed so much. I am happy to be part of this discussion :heart:

asvln commented 3 years ago

Are we at a consensus that the best course of action is to translate css directly into seed as it currently exists, leaving the option for tailwind/theming systems as separate crates?

It would make the API stable for-so as long as the CSS standards are.

I also am for the module based naming and lowercase macros as @TatriX has suggested:

Here is the cleanest interpretation I can fathom.

// Importing local fonts is a bit more complicated as it is not generally put inline.
let fira_code = css::font_face![
   css::font_family("Fira Code"),
   attr::src(attr::url("/fonts/fira-code.ttf")),
   css::font_weight(400),
]

// perhaps there can be a `root_css![]` which is used for such situations?
root_css![
    fira_code,
    descendant!("body", &[
       margin(0)
    ]
]

custom![
    tag::custom("custom-element"),
    attr::class("my-class"),
    css::background_color("green"),

    // rather than list![]
    attrs![
        id("my-container"),
        class!["container",  when!(active => "active")],
    ],
    css![
        position(Position::Relative),
        flex_grow(1),
        width((0, Auto)),
        child("a", &[
            font_family("Fira Code"),
            color("red"),
        ]),
    ],
]

We could copy the @media API with a media! macro.

MartinKavik commented 3 years ago

Hi guys!

I was rewriting one Seed app from Less to Seed Styles. Seed Styles API is still a bit rough, but usable enough. So I've created a new repo - github.com/seed-rs/styles_hooks - where we can focus on Styles and improve it. It should be a good starting point for the API designed in this discussion. This way we can experiment and try new css / Style API without changes in the Seed.

I can imagine that we can slowly rewrite it to css::-like API and recycle already implemented good patterns. However I found one thing that I'm not sure how to resolve:

Let's have a LESS styles with the code like:

LESS file ```less .meta-item-container { position: relative; overflow: visible; &:hover, &:focus, &.active, &.selected { outline-style: none; &::after { position: absolute; outline-width: var(--focus-outline-size); outline-color: var(--color-surfacelighter); outline-offset: calc(-1 * var(--focus-outline-size)); pointer-events: none; content: ""; } .title-bar-container { background-color: var(--color-surfacelight); .title-label { color: var(--color-backgrounddarker); } .multiselect-container { .multiselect-label-container { .icon { fill: var(--color-backgrounddarker); } } } } } ```

How should we rewrite it to Rust? CSS doesn't have parent selectors and the set of active pseudo-classes is stored implicitly in the browser. It makes it hard to split the code to components. One naive solution would be to listen for events like focus / blur and keep a variable is_focused in local / global state and pass it to sub-components/views. What do you think?

TatriX commented 3 years ago

Where can I look at the full code examples?

nielsle commented 3 years ago

Would it be possible to solve this in three steps?

MartinKavik commented 3 years ago

@nielsle Could you write a simple example? Note: I think SCSS is more popular than LESS and there are Rust SCSS/SASS compilers - so we should draw inspiration from SCSS rather than LESS.

nielsle commented 3 years ago

Hmm.. perhaps I should have formulated my message differently, but it could be interesting to see how much can be implemented with slices of enum variants. As far as I can see something like following could work.

let on_hover = selector!(Hover, &[
     css::color(Red)
]);

// Perhaps distinguish between css rules and descendants
root_css![
     descendant!(".meta-item-container",&[
         css::position(Relative),
         css::overflow(Visible),
         on_hover 
    ])
]

This approach works well in simple situations, because inheritance is just joining vectors, and I like that the CSS end up looking similar to HTML macros, but I must admit that I cannot predict all the corner cases.

MartinKavik commented 3 years ago

I found another possible solution in Tailwind - they use groups, see:

Probably there is a way to elegantly integrate this concept to Styles.

Related: