camertron/rux

Feature request: css code transpiling

Opened this issue ยท 12 comments

Same to css-in-js it would be fine to have bundled css from my rux component too.
Let's say, we have this folder structure:

my_component
--my_component.rux
--my_component.css
--my_component.rcss

And my_component.rux looks like this

class Ui::Mix::Home::SpacecraftItemComponent::SpacecraftItemComponent < ViewComponent::Base
  include Birdel::Component

  Styles = Data.define(
    :wrapper,
    :avatar,
    :name
  )
  attr_reader :spacecraft
  def initialize(spacecraft:)
    @spacecraft = spacecraft
  end

  def styles
    @styles ||= Styles.new(
      wrapper: "wrapper",
      avatar: "avatar",
      name: "name"
    )
  end

  def call
    <div class={css_class} data-controller={css_class}>
      <div class={styles.wrapper} data-action={events.handleClickItem}>
        <div class={styles.avatar}>
        </div>
        <div class={styles.name}>
          {spacecraft.name}
        </div>
      </div>
    </div>
  end
end

And my_component.rcss like this:

.wrapper{
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 8px;
  padding: 10px;
}
.avatar{
  width: 32px;
  height: 32px;
  border-radius: 15px;
  background-color: #001eff;
  background-size: 75%;
  background-position: center;
  background-repeat: no-repeat;
  background-image: var(--some-avatar-url)
}
.name{
  font-size: 14px;
}

And while Rux transpiles .rux component - it also transpiles .rcss file from this component to .css file, which may looks like this after:

.wrapper_pvsbv6dH6Fdf4{
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 8px;
  padding: 10px;
}
.avatar_87dGHbdfhb{
  width: 32px;
  height: 32px;
  border-radius: 15px;
  background-color: #001eff;
  background-size: 75%;
  background-position: center;
  background-repeat: no-repeat;
  background-image: var(--some-avatar-url)
}
.name_hgv6Hbdf{
  font-size: 14px;
}

As you can see - now our css selectors is uniq.
Why we need that - well, this is super simple example, but let's imagine we have 50 components. Each of them may have .wrapper or .avatar or anything same classes. Now solving that will looks like nesting css selectors:

.ui--mix--home--spacecraft-item-component
> .wrapper > .avatar{}

.ui--mix--home--spaceship-item-component
> .wrapper > .avatar{}

etc

By this way our bundle will have ton of KB lol
Or another way same to:

.spacecraft-avatar{}

.spaceship-avatar{}

etc

This way so fast will make our code super ugly. So, my proposal is realise transpiling rcss to css which will have uniq version of selectors so our code will be clean and our bundle will be super light weight.

Nice, I like this idea! I've wanted to add it for a while now.

A few questions:

  1. How do you get sprockets, webpacker, dart-sass, etc to include these .css files in the bundle?
  2. Since CSS transpiling isn't specific to rux, what if we put it in a separate gem? Maybe we could call it vcss for view_component CSS.
  3. I would like to avoid using hashes in CSS class names, eg. .wrapper_pvsbv6dH6Fdf4 because I personally find them hard to read. I wonder if we could prefix every class with the full constant name of the component, eg. .ui--mix--home--spacecraft-item-component > .ui--mix--home--spacecraft-item-component-wrapper > .ui--mix--home--spacecraft-item-component-avatar. That's obviously a lot more verbose, but easier to understand when you see one of those names in the Chrome inspector, for example.

Let me know your thoughts!

@camertron So cool you like my proposal!
Hmm, i using Birdel to automate my assets sync before bundling. But it doesn't matter if you uses Birdel or writing that manually, result entrypoint file may looks something like this:

//app/assets/stylesheets/ui/entries/home/index.css
@import url("../../united/colors.css");
@import url("../../united/images.css");
@import url("../../../../../components/ui/mix/home/space_home_component/space_home_component.css");
@import url("../../../../../components/ui/mix/home/spacecraft_list_component/spacecraft_list_component.css");
@import url("../../../../../components/ui/mix/home/spacecraft_item_component/spacecraft_item_component.css");

Next, esbuild makes bundling for each entrypoint by config which may looks like this:

//package.json
    "build:css": "esbuild app/assets/stylesheets/ui/entries/**/index.css --bundle --minify --outdir=app/assets/builds --outbase=app/assets/stylesheets --loader:.woff2=file --loader:.svg=file --external:app/assets/fonts/ui/* --external:app/assets/images/ui/*",

Or sometimes i can bundle some components styles too, it may help if you for example uses rails-turbo (which i personally don't like and uses Birdel instead)
So last step is Propshaft uses that bundled entries (or/and bundled components css files) for assets delivery, that's why i can't answear about dart-sass, sprockets etc

About another gem - personally I more like including a module based on some config in project like .config.vcss = true

And about hashes - you are right, it may 100% better to see normally named css classes like .ui--mix--home--spacecraft-item-component_wrapper while you develops app. But what about production? In rails is really problem is big bundle file, so if I could choose - I would definitely choose .wrapper_hb34JHG34 classes looking style for minimise my bundle

result entrypoint file may looks something like this:

Ah ok so you add them manually for each component. I wonder if there's a way to automate that? It would also be really cool if the page only requested the CSS it actually needed based on the components that have been rendered.

With regard to esbuild, propshaft, etc... it feels like automatic discovery of .css files might be hard to implement consistently for all these build systems. If the page only requests the component CSS it actually needs, maybe Sprockets would be the right build system to use.

About another gem - personally I more like including a module based on some config in project like .config.vcss = true

I'm not sure what you mean, can you explain in more detail?

And about hashes ... But what about production? In rails is really problem is big bundle file, so if I could choose - I would definitely choose .wrapper_hb34JHG34 classes looking style for minimise my bundle

That's a good point... although as I said above, I think it makes more sense for the page to request individual CSS files per component rather than bundle all of them up into one giant CSS file. With HTTP/2, gzip, etc, I think per-component CSS files could be pretty small and wouldn't impact load times dramatically.

Quick answear about .config.vcss = true means insert this css transpile feature inside rux gem to not create another one, and user can enable or disable this css transpile feature by config flag .config.vcss = true for example
Great next: i think it doesn't matter which tool user uses to create a bundle file or asset delivery, not?
The essence of VCSS is to take a classes from a my_component.vcss file based on @styles instance and transpile each class to .avatar_3hkb87HFy or nested version .ui--mix--home--spacecraft-item-component > .ui--mix--home--spacecraft-item -component-wrapper > .ui--mix--home--spacecraft-item-component-avatar and after that rewrite my_component.css component file with that classes and also keep it in inside html like:

<div class="ui--mix--home--spacecraft-item-component" data-controller="ui--mix--home--spacecraft-item-component">
  <div class="ui--mix--home--spacecraft-item-component-wrapper" data-action="click->ui--mix--home--spacecraft-item-component#handleClick">
    <div class="ui--mix--home--spacecraft-item-component-avatar">
    </div>
    <div class="ui--mix--home--spacecraft-item-component-name">
      Bla Bla
    </div>
  </div>
</div>

<!--OR Hashed version-->

<div class="ui--mix--home--spacecraft-item-component" data-controller="ui--mix--home--spacecraft-item-component">
  <div class="wrapper_jhb45" data-action="click->ui--mix--home--spacecraft-item-component#handleClick">
    <div class="avatar_6fjGFD6">
    </div>
    <div class="name_kj5fHG0">
      Bla Bla
    </div>
  </div>
</div>

You can see we changed only classes which was inside @styles instance and didn't changed classes which user wrote manually by another way without @styles instance
Maybe will be nice also set a config flag which version of transpiled classes you need .avatar_3hkb87HFy or .ui--mix--home--spacecraft-item-component > .ui--mix--home--spacecraft-item -component-wrapper > .ui--mix--home--spacecraft-item-component-avatar for example config.vcss_hashed = true

As you can see it doesn't matter for this feature how user bundles styles, by each component or by one big entry bundle, that's not our trouble haha๐Ÿ˜…

@camertron Have you think about that? Otherwise it would be fine to have some transpile callback, so users can realize this or anything else by himself
Like before_transpile, after_transpile etc

@camertron bump๐Ÿ‘†

Quick answear about .config.vcss = true means insert this css transpile feature inside rux gem to not create another one

Hmm I'm not sure I understand. Where does this config value get set? In application.rb maybe?

class Application < Rails::Application
  config.rux.vcss = true  # is this correct?
end

I think it doesn't matter which tool user uses to create a bundle file or asset delivery, not?

I'm not sure, I don't have much experience with all the new asset options in Rails these days. We just need to make it easy to configure whatever tool to process CSS files. If you think that doesn't require any magic on rux's part, then cool ๐Ÿ˜Ž

You can see we changed only classes which was inside @styles instance and didn't changed classes which user wrote manually by another way without @styles instance.

Right yeah that's great ๐Ÿ˜„ Would rux look for an @styles instance variable? It might be nicer to do something like this:

class Component < ViewComponent::Base
  styles do
    Styles.new(
      wrapper: "wrapper",
      avatar: "avatar",
      name: "name"
    )
  end
end

What do you think?

Maybe will be nice also set a config flag which version of transpiled classes you need

Ahh yes I like that! Make it easy to switch between them in case you want one style or the other ๐Ÿ‘

Ohh @camertron i totally agree with you. I think this way is cool:

styles do
  Styles.new(
    wrapper: "wrapper",
    avatar: "avatar",
    name: "name"
  )
end

And about configuration: i think it can be in development.rb file

Rails.application.configure do
  config.rux.vcss = true
  ...

Do you think that's possible?๐Ÿ˜„ I really need that and i'm sure that's a revolutionize feature

I think this way is cool

Awesome!

And about configuration: i think it can be in development.rb file

Right ok that makes sense. I still think this functionality should eventually live in a separate gem, but we can always extract it later.

Do you think that's possible?๐Ÿ˜„ I really need that and i'm sure that's a revolutionize feature

Yes, I definitely think it's possible! Are you volunteering to work on it?

@camertron I don't think i have time for that, but if i can help somehow a little - let me know๐Ÿ˜„

Hey @camertron can i help somehow to make this feature?๐Ÿ˜…

Hey @serhiijun, sure, feel free to dive in and submit a PR ๐Ÿ˜„ Maybe a good place to start is adding the styles method?