5

Sveltish

 3 years ago
source link: https://github.com/davedawkins/Fable.Sveltish
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Sveltish

An experiment in applying the design principles from Svelte to native Fable. Svelte is impressive in its own right, but I can't help thinking that Fable is a compiler that's already in our toolchain, and is able to do what Svelte does with respect to generating boilerplate.

It's all very much a work-in-progress, and I'm exploring what's possible, so the code is a sprawling mess. If this is worth pursuing, then it will need refactoring and organizing.

Some aspects that are working or in progress.

Check the end of this README to see news on progress.

DOM builder

Crude and minimal. It's Feliz-styled, but builds direct into DOM. If this project proceeds it would be good to layer on top of Feliz.

    div [
        class' "container"
        p [ text "Fable is running" ]
    ]

Stores

Similar to Svelte stores, using the same API

    let count = Sveltish.makeStore 0
    button [
      class' "button"
      onClick (fun _ -> count.Value() + 1 |> count.Set)
      count.Value() |> sprintf "You clicked: %i time(s)" |> text
    ]

Bindings

The intention is to have Fable or a Fable plugin analyze the AST and produce bindings automatically. F# even in my inexperienced hands does an amazing job of reducing boilerplate to a minimum, but it's still boiler plate.

The button example above won't yet update on button clicks. Here's how we make that happen:

    let count = Sveltish.makeStore 0
    button [
      class' "button"
      onClick (fun _ -> count.Value() + 1 |> count.Set)
      count.Value() |> sprintf "You clicked: %i time(s)" |> text
    ]

    (fun () -> count.Value() |> sprintf "You clicked: %i time(s)" |> text)
       |> bind count

It's ugly, but with Fable's help that can be made to look this:

    let count = Sveltish.makeStore 0
    button [
      class' "button"
      onClick (fun _ -> count + 1 |> count.Set)
      count |> sprintf "You clicked: %i time(s)" |> text
    ]

Styling

Working like Svelte. Here's how the Svelte animation example is coming along with respect to the styling.

Todos Progress
let styleSheet = [
    rule ".new-todo" [
        fontSize "1.4em"
        width "100%"
        margin "2em 0 1em 0"
    ]

    rule ".board" [
        maxWidth "36em"
        margin "0 auto"
    ]

    rule ".left, .right" [
        float' "left"
        width "50%"
        padding "0 1em 0 0"
        boxSizing "border-box"
    ]

    // ...
]

let view =
    style styleSheet <| div [
        class' "board"
        input [
            class' "new-todo"
            placeholder "what needs to be done?"
        ]

        todosList "left" "todo" (fun t -> not t.Done) |> bind todos
        todosList "right" "done" (fun t -> t.Done) |> bind todos
    ]

Transitions

Working on these right now. The key is being notified of a change in an element's visibility. The DOM intends to listen to a visibility expression (a Store<bool>) and then update style display: none|<not-none>;. Like a call to $.show() in you-know-what.

Transitions Progress

Here's the code for this component:

let Counter attrs =
    let count = Sveltish.makeStore 0
    div [
        button [
            class' "button"
            onClick (fun _ ->
                console.log("click")
                count.Value() + 1 |> count.Set)

            // Boiler plate to be generated by Fable plugin
            (fun () ->
                text <| if count.Value() = 0 then "Click Me" else count.Value() |> sprintf "You clicked: %i time(s)"
            ) |> bind count
        ]

        button [
            class' "button"
            Attribute ("style", "margin-left: 12px;" )
            onClick (fun _ -> 0 |> count.Set)
            text "Reset"
        ]

        // More boilerplate that can be generated automatically
        (div [ text "Click button to start counting" ])
        |> transition
                (InOut (Transition.slide, Transition.fade))
                (count |~> exprStore (fun () -> count.Value() = 0))  // Visible if 'count = 0'

    ]

The transition wrapper manages visibility of the contained element, according to the expression. It then uses the specified transitions to handle entry and exit of the element from the DOM.

We now have fade, fly and slide transitions

fly.gif
(Html.div [ className "hint"; text "Click button to start counting" ])
|> Bindings.transition
        (Both (Transition.fly,[ X 100.0; Y 100.0; ]))
        (count |~> exprStore (fun () -> count.Value() = 0 && props.ShowHint))  // Visible if 'count = 0'

We also have an each control that manages lists. Items that appear in, disappear from and move around in the list can be transitioned:

transfade.gif
let todosList cls title filter =
    Html.div [
        className cls
        Html.h2 [ text title ]

        Bindings.each todos (fun (x:Todo) -> x.Id) filter (Both (Transition.fade [])) (fun todo ->
            Html.label [
                Html.input [
                    attr ("type","checkbox")
                    Bindings.bindAttr "checked"
                        ((makePropertyStore todo "Done") <~| todos)
                ]
                text " "
                text todo.Description
                Html.button [
                    on "click" (fun _ -> remove(todo))
                    text "x"
                ]
            ]
        )
        
    ]

I'm looking forward to seeing how much boilerplate we can remove with a compiler plugin.

Crossfade is now working. This animation is deliberately set to run slowly so that I could check the behaviour. The final part of this example is the animate:flip directive.

crossfade.gif

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK