/Fun.Css

Use FSharp to build inline css style. Running in dotnet, fable.

Primary LanguageF#

Fun.Css Nuget

First, let`s check how it can look like:

style {
    backgroundColor "#44c767"
    borderRadius 30
    borderWidth 1
    borderStyleSolid
    borderColor "#18ab29"
    displayInlineBlock
    cursorPointer
    fontSize 17
}

Benchmarks (I know it is not fair comparison for Fss because Fss is more type safety and will automatically generate classname for you. But I did not find similar libraries to compare, just take as a reference), You can check the code in Benchmark/Benchmarks.fs:

Method Mean Error StdDev Median Gen 0 Gen 1 Allocated
BuildStyleWithFunCss 481.0 ns 20.27 ns 55.83 ns 462.5 ns 0.3266 - 1 KB
BuildStyleWithFeliz 722.6 ns 14.00 ns 14.38 ns 726.4 ns 0.4778 - 2 KB
BuildStyleWithFss 17,913,801.0 ns 350,775.83 ns 700,537.24 ns 17,668,715.6 ns 906.2500 31.2500 3,821 KB

This project is built in Fun.Blazor at first to help build inline style with type safety way.

Before I was using Feliz.Engine, when I was migrating Fun.Blazor to use InlineIfLambda for better performance, I found I can also make style building faster with the same way. So copied the Feliz.Engine basic methods for css and rebuild with computation plus InlineIfLambda.

The basic stuff is like this:

[<CustomOperation("color")>]
member inline _.color([<InlineIfLambda>] comb: CombineKeyValue, color: string) =
    comb &>> ("color", color)

CombineKeyValue is defined as:

type CombineKeyValue = delegate of StringBuilder -> StringBuilder

So after you build with release mode, everything should combined in a local functions with a StringBuilder provide to append all the string pieces together.

How to use it in your project

It depends, take Fun.Blazor as an example, I will just inherit Fun.Css.CssBuilder and add a new Run member to generate the final result. In my case it is a AttrRenderFragment

type StyleBuilder() =
    inherit Fun.Css.CssBuilder()

    member inline _.Run([<InlineIfLambda>] combine: Fun.Css.Internal.CombineKeyValue) =
        AttrRenderFragment(fun _ builder index ->
            let sb = stringBuilderPool.Get()
            builder.AddAttribute(index, "style", combine.Invoke(sb).ToString())
            stringBuilderPool.Return sb
            index + 1
        )

// With a helper function
let style = StyleBuilder()

Then I can use it in Fun.Blazor like this:

div {
    style { 
        color "red"
        height 100
        width 100
    }
}

Another example is just to generate a string for the style, then you can similar do things like:

type StyleStrBuilder() =
    inherit Fun.Css.CssBuilder()

    member inline _.Run([<InlineIfLambda>] combine: Fun.Css.Internal.CombineKeyValue) =
        let sb = stringBuilderPool.Get()
        let str = combine.Invoke(sb).ToString()
        stringBuilderPool.Return sb
        str

// With a helper function
let styleStr = StyleStrBuilder()
For Fable + React, it does not support, because as what I know React is using an js object for the inline style. So the key value is not the pure css standard instead it use camelCase. 
But you can use it in Fable to build pure css inline style string if you want.

TODO

[x] Add css selector, pseudo etc. (help wanted 😊)

But we may not need to build that, because it looks pretty complex and very flexible. Maybe we can just do this:

styleElement {
    ruleset ".selected span:hover" {
        color "red"
    }
}

And it it generate things like

<style>
    .selected span:hover {
        color: red;
    }
</style>

Even there is no type safety for the selector and pseudo class or element, but it is very straightforward to do.