Zaid-Ajaj / Feliz

A fresh retake of the React API in Fable and a collection of high-quality components to build React applications in F#, optimized for happiness
https://zaid-ajaj.github.io/Feliz/
MIT License
531 stars 77 forks source link

My journey with Feliz | A comparison between Fable.React and Feliz #155

Open MangelMaxime opened 4 years ago

MangelMaxime commented 4 years ago

Hello,

this issue is for sharing my experience with Feliz and Feliz.Bulma.

This feedback is based on converting Fable Repl from Fable.React+Fulma to Feliz+Feliz.Bulma.

In the first section, I will make a listing of all the things I liked or had problems with when using Feliz. I will do something similar for Fable.React and Fulma because they are not exempt from good and less good part.

Then, in a second time, I will try to explain how went my experience with Feliz and Feliz.Bulma. The goal is to share with you how my point of view changed over time and why.

Important

I know that the subject I am analysing is sensitive and that something that afraid me. But please, remember to keep the comment section positive.

Table of content

Categorization system

I tried to find a way to organise my feedbacks, the classic "pros vs cons" felt too limited and aggressive to me.

Instead, I will be using symbols:

The same entry can have several symbols :)

Feliz


✔️ Feliz and Fable.React can be mixed because Feliz is a layer on top of Fable.React


✔️ The code is more comfortable to indent compared to Fable.React because we only have a single list. It is easier to follow the tabulation.

Click here for details

**Fable.React + Fulma** ```fs div [ ] [ Hero.hero [ Hero.IsFullHeight ] [ Hero.body [ ] [ Container.container [ ] [ img [ Src "img/fable-ionide.png" Style [ Display DisplayOptions.Block Width "auto" Margin "auto" ] ] br [ ] Heading.h3 [ Heading.Modifiers [ Modifier.TextAlignment (Screen.All, TextAlignment.Centered) ] ] [ str "Fable REPL" ] Heading.p [ Heading.IsSubtitle Heading.Is5 Heading.Modifiers [ Modifier.TextAlignment (Screen.All, TextAlignment.Centered) ] ] [ str "is only available on desktop" ] ] ] ] ] ``` **Feliz + Feliz.Bulma** ```fs Html.div [ Bulma.hero [ hero.isFullHeight prop.children [ Bulma.heroBody [ Bulma.container [ Html.img [ prop.src "img/fable-ionide.png" prop.style [ style.display.block style.width length.auto style.margin length.auto ] ] Html.br [ ] Bulma.title3 [ text.hasTextCentered prop.text "Fable REPL" ] Bulma.subtitle5 [ text.hasTextCentered prop.text "is only available on desktop" ] ] ] ] ] ] ```


✔️ Thanks to the previous point it's also easy to move code around

feliz_move_code_demo


✔️ Strongly typed API for the DOM properties


✔️ Strongly typed API for CSS and CSS units via ICSSUnits

style.marginLeft (length.em 0.5)
style.width (length.percent (model.PanelSplitRatio * 100.))

✔️ Doesn't pollute the context, most of the things are under Html.* or Prop.*


✔️ ⚠️ Feliz offers the possibility to avoid noise in the code

Click here for details

```fs Html.span "Fable REPL" // instead of // Feliz verbose version span [ str "Fable REPL" ] // Fable.React span [ ] [ str "Fable REPL" ] // -------------------- Bulma.title4 "Fable REPL" // instead of // Feliz.Bulma verbose version Bulma.title4 [ prop.text "Fable REPL" ] // Fable.React Headings.h4 [ ] [ str "Fable REPL" ] ``` Because there are several ways to write the same code, we can't just "read" the code. We sometimes need to take a step back to understand the outer context. This also makes the code less "consistent" because not everything is written the same way. ```fs Html.tr [ Html.th "Steps" Html.th [ prop.className "has-text-right" prop.text "ms" ] ] ```


⚠️ It doesn't support (yet) SSR


⚠️ Discovering the methods/properties overload is not easy (this is perhaps a limitation of Ionide I don't know)

I had to:


⚠️ Make harder the use of callback taking more than one parameter. In Fable.REPL, I had to force the uncurry by using System.Func<_,_,_>.

Click here for details

```fs [] type editor = ///

Triggered when the Editor has been mounted static member inline editorDidMount (f : System.Func) = Interop.mkAttr "editorDidMount" f ``` Which gives this code on the calling side: ```fs let private registerCompileCommand dispatch = System.Func<_,_,_>( fun (editor : Monaco.Editor.IStandaloneCodeEditor) (monacoModule : Monaco.IExports) -> let triggerCompile () = StartCompile None |> dispatch editor.addCommand(monacoModule.KeyMod.Alt ||| int Monaco.KeyCode.Enter, triggerCompile, "") |> ignore editor.addCommand(monacoModule.KeyMod.CtrlCmd ||| int Monaco.KeyCode.KEY_S, triggerCompile, "") |> ignore ) ```


✔️ ⚠️ Feliz provides overloads for making user life a bit easier, but this has a cost.

For prop.onChange, as like 6 overloads depending on what you want to listen:

This is nice because this avoids people to write "no" fun code, but the counterpart is you need to say which Event type you want explicitly.

// Feliz
prop.onChange (fun (e : Types.Event) -> e.Value |> ChangeGistToken |> dispatch)

// Fable.React
prop.onChange (fun e -> e.Value |> ChangeGistToken |> dispatch)

✔️ ⚠️ Feliz ecosystem is mostly type-safe but doesn't prevent writing invalid code. When using Feliz and one of its extension like Feliz.Bulma you can easily mix properties but you need to be careful when doing it.

Click here for details

This code seems OK from Feliz and the F# compiler point of view but it will not give you the expected result. ```fs Html.p [ text.isUppercase text.isItalic color.hasTextSuccess prop.text "Hello Feliz" ] ``` `text.isUppsercase`, `text.isItalic` and `color.hasTextSuccess` all output something like `ClassName "my-css-class`. But in React, only the last one will have an effect so in our case the code will generate: ```html

Hello Feliz<\p> ``` instead of ```html


ℹ At first I thought Feliz didn't offer the syntax sugar ev.Value that we get when using Fable.React, but that's not the case.

Fable.React.Extension host the syntax sugar, so we can just open it to not pollute the context with all the Fable.React functions.

open Feliz
open Fable.React.Extensions

ℹ The syntax doesn't reproduce the HTML way of thinking. Feliz is more a syntax sugar on top of React API than HTML. Feliz thinks in term of properties because even children is a property.


Feliz.Bulma

✔️ Integrates well with Feliz


✔️ Easier to indent compared to Fulma

Click here for details

```fs // Fulma Card.card [ ] [ Card.header [ Common.Props [ OnClick (fun _ -> ToggleWidget title |> dispatch ) ] ] [ Card.Header.title [ ] [ Icon.icon [ Icon.Props [ Style [ MarginRight ".5em" ] ] ] [ Fa.i [ icon; Fa.Size Fa.FaLarge ] [] ] str title ] Card.Header.icon [ ] [ Icon.icon [ ] [ Fa.i [ headerIcon ; Fa.Size Fa.FaLarge ] [] ] ] ] ofOption content ] // Feliz.Bulma Bulma.card [ Bulma.cardHeader [ prop.onClick (fun _ -> ToggleWidget title |> dispatch ) prop.children [ Bulma.cardHeaderTitle [ Bulma.icon [ prop.style [ style.marginRight (length.em 0.5) ] prop.children [ Fa.i [ icon; Fa.Size Fa.FaLarge ] [] ] ] Html.text title ] Bulma.cardHeaderIcon [ Bulma.icon [ Fa.i [ headerIcon ; Fa.Size Fa.FaLarge ] [] ] ] ] ] Html.ofOption content ] ```


✔️ It's easy to identify Bulma components thanks to Bulma prefix


✔️ ⚠️ The properties are accessible but pollute the context button.*, help.*, columns.*.

In theory, people should only use one CSS framework, so I don't think they will have a clash on generic properties like button.*, columns.*, etc.


✔️ The component hierarchy seems easy enough to grabs

Bulma.card > Bulma.cardHeader > Bulma.cardHeaderTitle

Click here for example

```fs Bulma.card [ Bulma.cardHeader [ prop.onClick (fun _ -> ToggleWidget title |> dispatch ) prop.children [ Bulma.cardHeaderTitle [ // ... ] Bulma.cardHeaderIcon [ // ... ] ] ] ] ```


⚠️ But some of the components don't follow the same convention

Bulma.passwordInput instead of Bulma.inputPassword

In this case, you can't easily explore the different type of input you have because they don't start with the same "prefix".


✔️ ⚠️ Feliz.Bulma makes it easy to mix components behaviours.

Click here for details

```fs Bulma.button [ // Properties specific to a button coming from Feliz.Bulma button.isOutlined // Properties specific to a tooltip coming from Feliz.Bulma tooltip.hasTooltipRight tooltip.text tooltipText // General properties coming from Feliz prop.disabled isCompiling prop.onClick (fun _ -> dispatch msg) prop.children [ Bulma.icon [ icon.isLarge prop.children faIcon ] ] ] ``` That's nice because as you see, it's easy to add new behaviour to an existing component. Here we added the behaviour tooltip to a button. But this also means you can write invalid code like: ```fs Html.select [ select.isFullwidth ] ``` instead of ```fs Bulma.select [ select.isFullwidth ] ``` Fulma is more strict about component separation and doesn't allow you to mix the behaviour unless you pass the CSS classes via `CustomClass` props.


✔️ ⚠️ Feliz.Bulma doesn't provide a way for controlling which HTML element we want to output.

For example, we only have Bulma.field which generates a div. However, sometimes you want a p element as the output.


✔️ ⚠️ Feliz.Bulma is an exact mapping over Bulma

This is nice because it's slim.

But it also means:

For example, when writing a Bulma.tabs components you are not guided and need to know that tabs need ul followed by li with an a in it.

Click here to see the code

```fs // Feliz + Feliz.Bulma Bulma.tabs [ tabs.isCentered tabs.isMedium tabs.isToggle prop.children [ Html.ul [ Html.li [ if (activeTab = CodeTab.FSharp) then prop.className "is-active" prop.onClick (fun _ -> SetCodeTab CodeTab.FSharp |> dispatch) prop.children [ Html.a [ prop.text "F#" ] ] ] Html.li [ if (activeTab = CodeTab.Html) then prop.className "is-active" prop.onClick (fun _ -> SetCodeTab CodeTab.Html |> dispatch) prop.children [ Html.a [ prop.text "HTML" ] ] ] Html.li [ if (activeTab = CodeTab.Css) then prop.className "is-active" prop.onClick (fun _ -> SetCodeTab CodeTab.Css |> dispatch) prop.children [ Html.a [ prop.text "CSS" ] ] ] ] ] ] ``` Fulma allows the user to think more in term of "Bulma components". ```fs // Fable.React + Fulma Tabs.tabs [ Tabs.IsCentered Tabs.Size Size.IsMedium Tabs.IsToggle ] [ Tabs.tab [ Tabs.Tab.IsActive (activeTab = CodeTab.FSharp) Tabs.Tab.Props [ OnClick (fun _ -> SetCodeTab CodeTab.FSharp |> dispatch) ] ] [ a [ ] [ str "F#" ] ] Tabs.tab [ Tabs.Tab.IsActive (activeTab = CodeTab.Html) Tabs.Tab.Props [ OnClick (fun _ -> SetCodeTab CodeTab.Html |> dispatch) ] ] [ a [ ] [ str "HTML" ] ] Tabs.tab [ Tabs.Tab.IsActive (activeTab = CodeTab.Css) Tabs.Tab.Props [ OnClick (fun _ -> SetCodeTab CodeTab.Css |> dispatch) ] ] [ a [ ] [ str "CSS" ] ] ] ```

Note:

Fulma considers tab as a component and offers Tabs.Tab.* specific wrapper.

Fulma still needs you to know that an a element is necessary, but we could add it by default (that's the case for some components).


✔️ ⚠️ Feliz.Bulma has a smaller impact on the bundle size on a small project but will have a more significant impact when the project size increase.

Click here for details

Fulma is using a lot of DUs to model Bulma classes. You can take a look at [Common.fs](https://github.com/Fulma/Fulma/blob/2f99474cd6c793776001d07da009f7211be2f30c/src/Fulma/Common.fs); also each component has its own DUs. This requires Fulma to implements a function call `parseOptions` which in simple terms convert the DUs into classes. Feliz.Bulma takes a more direct approach by not creating a DSL on top of Bulma classes but instead directly output the classes. ```fs // Fulma Column.column [ Column.Width (Screen.Desktop, Column.IsHalf) Column.Width (Screen.Mobile, Column.IsFull) ] [ // ... ] // Feliz Bulma.column [ column.isHalfDesktop column.isFullMobile prop.children [ // ... ] ] ``` Thanks to the direct class usage and the manipulation of the property list Feliz.Bulma doesn't have the big cost of all the DUs and code added in Fulma. However, it still needs an extra pass to "join" all the classes. This part is done via [Feliz.Bulma.ElementBuilders.Helpers module](https://github.com/Dzoukr/Feliz.Bulma/blob/3ecbba1579d2a26281f24e6a6664b5d9c5222603/src/Feliz.Bulma/ElementBuilders.fs#L6-L23) but all these functions are inlined. That's why the bigger your project, the bigger the impact on your code.


⚠️ It is not easy to add new colour support.

In Feliz.Bulma, each component holds it's own colour implementation like button.isWarning, help.isWarning, etc.

So if you want to add your colour, you need to implement all button.isMyNewColor, help.isMyNewColor

In Fulma, they all share the same color type. See the documentation


Fable.React

✔️ Follows HTML structure when you think of it as a list of properties and a list of children


⚠️ Indentation rules are hard to establish because we need to organise a double list and in general, we make a lot of exceptional cases.

Click here for a detailed explanation

Example: ```fs // When I want to put an empty div div [ ] [ ] // When I want to put a div with a single property div [ ClassName "button" ] [ ] // or div [ ClassName "button" ] [ ] // When I want to put a div with several property div [ ClassName "button"; Id "my-button" ] [ ] // yes but what if one of my property is not that simple? div [ ClassName "button" OnClick (fun _ -> // do something ) ] [ ] // It's also possible to have consistent indentation if you do something like that div [ ] [ // Children 1 // Children 2 ] // But if you have non simple property you still have problems, I don't personally don't find it easy to read // I am not sure if that's how people would write it because I don't use this format personally div [ ClassName "button" OnClick (fun _ -> // do something ) ] [ div [ ] [ str "Children 1" ] ] // and so on ``` As you can see just when dealing with a few cases on the property list, we have several possible syntax. If I had to establish a consistent syntax in my project, it would be something like that: ```fs div [ // Property 1 ... // Property 2 ... // Property 3 ... ] [ div [ // Property 1 ... // Property 2 ... // Property 3 ... ] [ // Children 1 ... // Children 2 ... // Children 3 ... ] div [ // Property 1 ... // Property 2 ... // Property 3 ... ] [ // Children 1 ... // Children 2 ... // Children 3 ... ] ] // So for an empty element div [ ] [ ] // Awesome... 🙄 ``` For comparaison in Feliz, I do: ```fs // Empty div Html.div [ ] // Non empty div Htmldiv [ // Property 1 // Property 2 // Property 3 prop.children [ Html.div [ // Property 1 // Property 2 // Property 3 ] Html.div [ // Property 1 // Property 2 // Property 3 // Complexe property OnClick (fun _ -> // Do something ) ] ] ] ``` So I have only 2 cases now, and I could even write the empty version on multiple lines without too much noise if I really want a single way of structuring Feliz code.


⚠️ Most of the API is not typed

type HTMLAttr =
    | DefaultChecked of bool
    | DefaultValue of obj
    | Accept of string
    | AcceptCharset of string
    | AccessKey of string
    | Action of string
    | AllowFullScreen of bool
    | AllowTransparency of bool

✔️ ⚠️ Some of the API is typed but not all, which means that the code is not consistent.


✔️ It supports SSR


Fulma

✔️ Type-safe API

Modifier.TextAlignment (Screen.All, TextAlignment.Centered)

Button.button [ Button.Color IsWhite ]
    [ str "White" ]

✔️ Fulma API is easy to explore via intellisense when you understand how it's structured


✔️ Fulma force you to think in term for components


✔️ Make it easy to extends the supported colour thanks to IsCustomColor

// All you need to add `custom-purple` support to all your components is this line
let isCustomPurple = IsCustomColor "custom-purple"

✔️ ⚠️ I think it has good documentation with examples for all the components, but not every component contains the same level of information


✔️ ⚠️ Fulma impact on your bundle size is "stable". This point has been described in Feliz.Bulma section


⚠️ People don't find "Special helpers" easily.


⚠️ Fulma code add noise to your code because of the strongly typed DSL

Click here for an example

```fs // Fulma Heading.p [ Heading.IsSubtitle Heading.Is5 Heading.Modifiers [ Modifier.TextAlignment (Screen.All, TextAlignment.Centered) ] ] [ str "is only available on desktop" ] // Feliz.Bulma Bulma.subtitle5 [ text.hasTextCentered prop.text "is only available on desktop" ] ```


⚠️ Fulma code is not that easy to format correctly because it has a lot of nested list when you combine Props, Modifiers


I have been working on this analysis on a period of 2-3 weeks and could still continue. But I have to end it because it's taking a lot of time and I think I now have a better view of the situation.

All that to say, that there could be more good points or problems to list especially about Fable.React and Fulma but that also the hardest part for me. I have been using them and designing them for more than 3 years so it's hard to have an objective view on them.

Was the conversion difficult?

The conversion from Fable.React to Feliz was easy enough to do. Feliz being compatible with Fable.React you can do the conversion in a progressive way file per file, component per component, etc.

To be sure I converted everything I removed all the open Fable.React and open Fable.React.Props instructions. I also removed Fulma as a dependency.

Of course, I had forgotten some files so I fixed the compiler errors and then it was working.

You can see the merge request hosting the conversion here.


My journey with Feliz

When discovering Feliz at first, I was on the defensive because it was not following the "HTML/JSX way" and it's an alternate project for things I invested a lot of effort in.

Then I remembered that it's how innovation works and that what allows us to grow. For example, thanks to that that we have Elmish today.

Another thing I remembered is that JSX is not the "real React API", indeed React API is React.createElement. JSX is a syntax sugar on top of it, the same thing goes for Fable.React and Feliz. I insist on it because for me it helped me move forward and be curious about it.

To test Feliz, I decided to use a medium size project which is the Fable REPL. Fable REPL is not a complex application but it uses a lot of feature from Fable and React.

A non-exhaustive list of things I have tested thanks to this decision:

When starting the conversion I was really pleased with how easy it was to format the code. I think the convention for writing Feliz based view could be really easy to follow. The bigger my applications grow, the more I find code formatting important.

Then in the middle, I started to be annoyed with something but couldn't find what.

I finished converting Fable REPL to Feliz, and wasn't sure if I liked or not my experience with it.

It's only now, that I trying to conclude my analysis, that I understand what holds me back. It is not much related to Feliz but more to Feliz.Bulma.

It's important to remember that for more than 2.5 years I have been working on Fulma and improving it. Fulma is one of my biggest projects. Writing code with it is great especially because everything is typed and it forces you to think in term of components.

Feliz.Bulma takes a different approach and is younger than Fulma; it's only 5 months old.

With all that said, I think it's now time to move to my conclusion :)

Conclusion

I think Feliz biggest advantage is the single list style. That's something I mentioned a lot but for me being able to format easily my code and not having to think where I should start the next line is a big plus.

Secondly, the type-safe API is a must-have. It is more verbose than the CSS equivalent but I think it's a good feature. I didn't do fancy stuff with it so I can't say if everything is supported but if that's not the case we can always make a PR to fix that :).

Feliz.Bulma shows a promising way to extends Feliz but is not yet mature enough to be a direct equivalent to Fulma. This is something I want to explore more with @Dzoukr and I hope we will be able to found a middle ground between the simplicity of Feliz.Bulma and the completeness but verbosity of Fulma.

3 years ago, the 8 Mar 2017, Tomas, Eugene and me decided to merge Fable.Arch with Elmish. This allowed us as a community to build the amazing ecosystem we have today.

I think we are currently living a similar period with Feliz and Fable.React. I think Feliz will gain more traction than Fable.React but both will co-exist together. In the end, both of them works well together this means people can choose the one they prefer to use.

Thank you for reading my analysis and I will be happy to discuss it with you in the comment section :)

l3m commented 4 years ago

Great analysis.

For me personally, even though the double list formatting is a pain sometimes, I feel that looking at Fable.React code I get to see the structure of the underlying html better. It's div [.. , not Html.div [.... (etc). It's not a big thing, but my brain seems to have an easier parsing Fable.React.

Shmew commented 4 years ago

Great write up @MangelMaxime, you made a lot of good points.

From what I've gathered the main takeaway of this is mostly issues with how the library is extended, and the problems that occur when not done optimally.

I can definitely agree that writing libraries for Feliz places a lot more of the work on the developer, but in my opinion it's worth the effort when the resulting api is so nice to use.

I've written a fair number of extensions for Feliz so I think I can provide some insight on how to mitigate a lot of these issues:

Overloads

I use Visual Studio and have zero problems finding which overloads are available to me, maybe this is something that can be improved on ionide's end?

We could just make prop.text be a single inline function that requires the member toString and accept any type?

Callbacks:

Instead of:

[<Erase>]
type editor =
    /// <summary>Triggered when the Editor has been mounted</summary>
    static member inline editorDidMount (f : System.Func<Monaco.Editor.IStandaloneCodeEditor, Monaco.IExports, unit>) = Interop.mkAttr "editorDidMount" f

Write:

[<Erase>]
type editor =
    /// <summary>Triggered when the Editor has been mounted</summary>
    static member inline editorDidMount (f : Monaco.Editor.IStandaloneCodeEditor -> Monaco.IExports -> unit) = Interop.mkAttr "editorDidMount" (Func<_,_,_> f)

You can then call the property like you would expect on the client side.

Writing invalid code

I haven't seen anyone else apply this quite to the extent I have in some of my libraries, and may not be entirely applicable when extending Html. The way you prevent this is by adding additional interface types to the properties you define, in my largest Feliz library Feliz.Plotly I have it setup in such a way that you will get a compiler error if you try to use a property that is not valid. The only downside is it's quite a bit of work for the library developer.

Click to expand ```fs [] module Types = type IPlotProperty = interface end type IModeBarButtonsProperty = interface end type IAaxisProperty = interface end type IAggregateProperty = interface end type IAggregationProperty = interface end type IAggregationsProperty = interface end type IAngularaxisProperty = interface end type IAnimationProperty = interface end type IAnnotationProperty = interface end type IAnnotationsProperty = interface end type IAreaProperty = interface end type IAspectratioProperty = interface end type IAxisProperty = interface end type IBarProperty = interface end type IBarpolarProperty = interface end type IBaxisProperty = interface end type IBorderProperty = interface end type IBoxProperty = interface end type IButtonProperty = interface end type ICameraProperty = interface end type ICandlestickProperty = interface end type ICapsProperty = interface end type ICarpetProperty = interface end type ICaxisProperty = interface end type ICellsProperty = interface end type ICenterProperty = interface end type IChoroplethProperty = interface end type IChoroplethmapboxProperty = interface end type ICircleProperty = interface end type IColoraxisProperty = interface end type IColorbarProperty = interface end type IColorscaleProperty = interface end type IColorscalesProperty = interface end type IConcentrationscalesProperty = interface end type IConeProperty = interface end type IConfigProperty = interface end type IConnectorProperty = interface end type IContourProperty = interface end type IContourcarpetProperty = interface end type IContoursProperty = interface end type ICumulativeProperty = interface end type ICurrentvalueProperty = interface end type IDecreasingProperty = interface end type IDeltaProperty = interface end type IDensitymapboxProperty = interface end type IDiagonalProperty = interface end type IDimensionProperty = interface end type IDimensionsProperty = interface end type IDomainProperty = interface end type IEditsProperty = interface end type IErrorXProperty = interface end type IErrorYProperty = interface end type IErrorZProperty = interface end type IEyeProperty = interface end type IFillProperty = interface end type IFilterProperty = interface end type IFontProperty = interface end type IFrameProperty = interface end type IFramesEntryProperty = interface end type IFramesProperty = interface end type IFunnelProperty = interface end type IFunnelareaProperty = interface end type IGaugeProperty = interface end type IGeoProperty = interface end type IGradientProperty = interface end type IGridProperty = interface end type IGroupbyProperty = interface end type IHeaderProperty = interface end type IHeatmapProperty = interface end type IHeatmapglProperty = interface end type IHistogram2dProperty = interface end type IHistogram2dcontourProperty = interface end type IHistogramProperty = interface end type IHoverlabelProperty = interface end type IImageProperty = interface end type IImagesProperty = interface end type IIncreasingProperty = interface end type IIndicatorProperty = interface end type IInsidetextfontProperty = interface end type IIsosurfaceProperty = interface end type ILabelfontProperty = interface end type ILataxisProperty = interface end type ILayerProperty = interface end type ILayersProperty = interface end type ILayoutProperty = interface end type ILeafProperty = interface end type ILegendProperty = interface end type ILightingProperty = interface end type ILightpositionProperty = interface end type ILineProperty = interface end type ILinkProperty = interface end type ILonaxisProperty = interface end type IMapboxProperty = interface end type IMarginProperty = interface end type IMarkerProperty = interface end type IMeanlineProperty = interface end type IMesh3dProperty = interface end type IModebarProperty = interface end type INodeProperty = interface end type INumberProperty = interface end type IOhlcProperty = interface end type IOutsidetextfontProperty = interface end type IPadProperty = interface end type IParcatsProperty = interface end type IParcoordsProperty = interface end type IPathbarProperty = interface end type IPieProperty = interface end type IPointcloudProperty = interface end type IPolarProperty = interface end type IProjectProperty = interface end type IProjectionProperty = interface end type IRadialaxisProperty = interface end type IRangebreakProperty = interface end type IRangebreaksProperty = interface end type IRangefontProperty = interface end type IRangeselectorProperty = interface end type IRangesliderProperty = interface end type IRotationProperty = interface end type ISankeyProperty = interface end type IScatter3dProperty = interface end type IScatterProperty = interface end type IScattercarpetProperty = interface end type IScattergeoProperty = interface end type IScatterglProperty = interface end type IScattermapboxProperty = interface end type IScatterpolarProperty = interface end type IScatterpolarglProperty = interface end type IScatterternaryProperty = interface end type ISceneProperty = interface end type ISelectedProperty = interface end type IShapeProperty = interface end type IShapesProperty = interface end type ISlicesProperty = interface end type ISliderProperty = interface end type ISlidersProperty = interface end type ISortProperty = interface end type ISpaceframeProperty = interface end type ISplomProperty = interface end type IStartsProperty = interface end type IStepProperty = interface end type IStepsProperty = interface end type IStreamProperty = interface end type IStreamtubeProperty = interface end type IStyleProperty = interface end type IStylesProperty = interface end type ISunburstProperty = interface end type ISurfaceProperty = interface end type ISymbolProperty = interface end type ITableProperty = interface end type ITernaryProperty = interface end type ITextfontProperty = interface end type IThresholdProperty = interface end type ITickfontProperty = interface end type ITickformatstopProperty = interface end type ITickformatstopsProperty = interface end type ITilingProperty = interface end type ITitleProperty = interface end type ITotalsProperty = interface end type ITracesProperty = interface end type ITransformsProperty = interface end type ITransitionProperty = interface end type ITreemapProperty = interface end type IUniformtextProperty = interface end type IUnselectedProperty = interface end type IUpProperty = interface end type IUpdatemenuProperty = interface end type IUpdatemenusProperty = interface end type IViolinProperty = interface end type IVolumeProperty = interface end type IWaterfallProperty = interface end type IXProperty = interface end type IXaxisProperty = interface end type IXbinsProperty = interface end type IYProperty = interface end type IYaxisProperty = interface end type IYbinsProperty = interface end type IZProperty = interface end type IZaxisProperty = interface end type IButtonsProperty = inherit IModeBarButtonsProperty type IMeasureProperty = interface end type ITemplateProperty = interface end ```

In a similar vein, you can also implement things in the library code to "automagically" combine the properties if you want. I'm not sure how this works when using things outside your own library though. The trick to this is to make those items a different type than just IReactProperty. For example in Feliz.Plotly I do a lot of things where depending on configurations given I will modify/collect/change the users input to properly translate it to what the actual code needs to look like. It is really nice in some respects for removing things that are always present.

Conclusion

I think much of the pain you had could be solved with better documentation on how to navigate writing your own libraries in the "Feliz" way. Maybe @Zaid-Ajaj and I can work on that in the future.

MangelMaxime commented 4 years ago

Thank you @l3m

For me personally, even though the double list formatting is a pain sometimes, I feel that looking at Fable.React code I get to see the structure of the underlying html better. It's div [.. , not Html.div [.... (etc). It's not a big thing, but my brain seems to have an easier parsing Fable.React.

Indeed, switching how to read the code is not easy and that's why I mentioned we should not see it as HTML in my case it's helped me make the switch.

I suppose that with F# 5 and the ability to "open static classes" Feliz will be able to support both Html.div and div syntax depending on if you open or not the Html class.

Thank you @Shmew for the comment and indeed I forgot that there are several types of "libraries" for Feliz.

About the "Writing invalid code" yes but in some libraries, we do want to "extends" IReactProperty type in order to avoid the Fulma situation where you need to pass standard properties via a special helper like Props. The reason being that Bulma "components" are just a specialization over the DOM elements.

They are just code that injects a class by default in the HTML element.

Bulma.button generates <div class="button">

In the case of a library which works on "real" components (sorry don't know how to name it differently) your solution is indeed the right one. For example, this is the approach taken by Fable.ReactLeaflet where I am defining the specific properties for each component. And thus components don't have to support all the standard HTML properties.

About your solution for "Callbacks" not it didn't work for me. But the function passed by Fable was having curried arguments and unless I wanted to write something like myFunction(arg1)(arg2) then I had to force the uncurried version via System.Func.

If you know how to make it work I will be happy to see a PR sent to fix it in https://github.com/fable-compiler/repl/pull/108/files

Zaid-Ajaj commented 4 years ago

Hi @MangelMaxime,

First of all, I want to thank you for taking the time and effort not just to try out Feliz on a project but also to write such an extensive analysis. I am sure many of Fable users will find it very helpful and informative.

I really appreciate that this kind of feedback and openness and believe it is crucial for to make this library the best it can be along with its ecosystem :pray:

I will talk about Feliz.Bulma in a moment because I think it is kind of special in the ecosystem, First I will answer to some issues you have that can already be fixed without changes in the library.

Overloads of onChange

you need to say which Event type you want explicitly.

Actually, you don't. The type should be inferred from your message type as follows:

prop.onChange (ChangeGistToken >> dispatch)

Depending on the type of ChangeGistToken, the correct overload will be inferred. When the DU case is ChangeGistToken of string then the overload of string -> unit will be chosen. But it can also be FileSelected of File when input type is file in which case the overload File -> unit will be inferred and so on and so forth. You don't need the syntax sugar of ev.Value from Fable.React

prop.onCheckedChange will be removed because onChange handles the boolean case already

Multi-argument callbacks

@Shmew has already touched upon the issue and indeed that is the way to solve these types of properties. It is crucial to understand that when implementing Feliz extensions, you can do transformations before setting the property:

type myExtension = 
    static member inline extenstionProperty value = 
        // transform value here
        let transformedValue = doWeirdStuffWith value
        Interop.mkAttr "extenstionProperty" transformedValue

Following this logic, you could convert 'A -> 'B -> unit into Func<'A, 'B, unit> internally:

[<Erase>]
type editor =
    /// <summary>Triggered when the Editor has been mounted</summary>
    static member inline editorDidMount (f : Monaco.Editor.IStandaloneCodeEditor -> Monaco.IExports -> unit) = 
        let transformedFunction = System.Func<Monaco.Editor.IStandaloneCodeEditor, Monaco.IExports, unit>(fun editor exported -> f editor exported)
        Interop.mkAttr "editorDidMount" transformedFunction

This is a more explicit version than the one provided by @Shmew (As you know with bindings, you always have to play with them a bit to get them right). A working example of this is here

Type-Safety in the Feliz ecosystem

As @Shmew mentioned above: a proper Feliz extension doesn't allow you to write code that behaves incorrectly and constraining the types of properties allow you to implement that. You can either build properties that return IReactProperty like Feliz.Recharts does which allow for backward-compatibility with existing properties in prop or you could implement a more specialized type ISpecialiedProperty to support only a subset of the properties while duplicating the names of some properties from IReactProperty to ensure backward-compatibility:

type felizExtension = 
    static member inline specializedProp (value: string) : ISpecializedProperty = unbox (prop.text value) 

This what I have been doing with Feliz.AntDesign, for example here where I am duplicating some properties to make available from the button entry point (Later on, I could make the properties more specialized with strict types but at first, backward-compat is where you first start). I believe Feliz.MaterialUI does similar things to extend IReactProperty rather than specializing properties. This brings to the case of Feliz.Bulma.

The case of Feliz.Bulma

Before Feliz.Bulma was implemented, I had in my mind the idea that there is no need to build Feliz extensions for CSS-only frameworks. Feliz makes it already very easy to compose multiple class names and work with them conditionally. Now with implicit yield in F# it is even easier. Combine this with TypedCssClasses and you remove the need of implementing any CSS-only framework. Zero implementation and maintenance costs. Just use the typed stylesheet and use the already really good documentation of the project itself instead of having to build a documentation website yourself.

However, after I saw the implementation of Feliz.Bulma by @Dzoukr it made sense: you could simplify the code a lot by introducing specific "entry points" to the CSS components from one module of Bulma so that you get Bulma.button, Bulma.card etc. These are really nice to use. As for the modifiers, I think Roman's approach is easy to follow and to work with but like you said, sometimes it could give interesting results. Surely, there is more to improve in that department and in any case I think Roman did a great job at following the vision of Feliz into building something easy to learn and to use.

Package management of Feliz

I just want to point out a huge difference in current Fable.React and Feliz: the package versioning and management. Feliz versions are all of its ecosystem libraries are Femto-compatible which removes the burden from users of having to manually install required npm packages or knowing which version they are compatible with. This is unlike many if not all derivative Fable.React libraries and even Fable.React itself where that is not the case.

Future Plans

MangelMaxime commented 4 years ago

Actually, you don't. The type should be inferred from your message type as follows:

prop.onChange (ChangeGistToken >> dispatch)

Indeed, I didn't think about this usage.

Multi-argument callbacks

@Shmew has already touched upon the issue and indeed that is the way to solve these types of properties.

Oh sorry, I didn't saw the end of the snippet provided by @Shmew. I just saw the signature declaration and was like no doesn't work.

You are both right, we can do the transformation before passing the value to the property.

The case of Feliz.Bulma

Yes, TypedCssClasses is ok but compared to Fulma or Feliz.Bulma it can't provide the same level of features. Because not everything is contained in the CSS file.

Another thing is that in dev mode, we don't always have the file generated on disk and I am not sure if TypedCssClasses support this scenario.

I think Roman did a great job at following the vision of Feliz into building something easy to learn and to use.

I do agree, I just think we need to add a bit of the work done in Fulma to make it similar. Like I said we need to find the correct middle ground between both of them.

The missing features I have in mind right now are allowing to control the output element and make it easier to extends the set of supported colour. I don't think it will add much complexity over it.

@Zaid-Ajaj thank you for the feedback and especially for pointing me the right direction on how to use Feliz potential. I admit I didn't really read Feliz documentation (that's bad I know) so perhaps they are documented in it. If not, I guess we know a few more things needed to add :)

theimowski commented 4 years ago

Thanks @MangelMaxime - that's a great comparison. You haven't mentioned Fantomas anywhere for auto formatting the code - did you have a chance to work with Fantomas with either Fable.React or Feliz? In one of my Fable projects where I use Fantomas, I turned on auto-formatting on file save in Ionide and must say it's really a productivity booster - no need to worry about manual formatting at all! I really think Fantomas is getting its momentum now and would love to see its wider adoption, also in conjunction with Fable and React / Feliz.

MangelMaxime commented 4 years ago

@theimowski I didn't use Fantomas since some times.

Mostly because it was messing with my code and making it really hard to read. From my experience to was not playing well with Fable.React and Feliz.

I also don't like the default of Fantomas I need to check it again in the future when I know how I want my code to be indented. But, from what I looked at the repo I don't think it will fit my needs mostly because of Fantomas formatting the code depending on the size of context.

For example, if I write something like that:

type Test =
    {
        Prop1 : string
    }

I do want it to stay like that and not become type Test = { Prop1 : string } just because it can fit on a single line.

Perhaps a record is not a good example, but I hope you understand what I mean :)

Shmew commented 4 years ago

@theimowski that's great to hear, I use fantomas on my non-Fable projects. I'll have to give it another shot and see how it handles it.

albertwoo commented 4 years ago

@MangelMaxime thanks for your work. Somehow your Fulma project attacked me to the fable world. And I really enjoy the journey.

I also like the single list but I also like DU than classes member overrides. Sometimes I wonder if DU supports override then it would be super great. Because I already use Fable.React a lot but still want single list, so I created a operator </> and I can use it like:

let myView =
    div </> [
        Classes [ ""; ""; "" ]
        Children [
              ...
        ]
    ]

The operator is pretty simple and also take advantage of DU, you can check my rough implementation here: https://github.com/albertwoo/Fun.LightForm/blob/master/src/Fun.LightForm.Fable/Fable.React.Extra.fs

@Zaid-Ajaj Thanks for your great work too, I also started to convert some of my projects to Feliz and Feliz.Material.

Thanks!

Dzoukr commented 4 years ago

Hi @MangelMaxime, first of all thank you! Frankly I wasn't expecting someone would have such deep look at library I created mainly for dogfooding own projects and published as side effect. Funny how initial thought of "hey, how @Zaid-Ajaj designed Feliz is great - let's do the same for Bulma" swollen up.

I already created new Issue in Feliz.Bulma repo and love to discuss how to make v2 even better. It seems we have agreement on most of issues, so it shouldn't be any problem to start version2 branch. Also looking at what are Zaid's plan for next major release could be handy to keep API between latest major versions 100% compatible.

Anyway, thank you again - great analysis!

isaacabraham commented 4 years ago

At the risk is reopening old discussions (sorry!) I just wanted to add my two cents on the indentation debate - this is (more or less) the styling we follow which does allow for nice, consistent identation and following for the Fable.React style.

The one place this doesn't quite apply to, however, is for non-trivial styling. However, in our experience this accounts for only a minority of elements (maybe 10-20%).

div [ ] [
    Hero.hero [ Hero.IsFullHeight ] [
        Hero.body [ ] [
            Container.container [ ] [
                img [ Src "img/fable-ionide.png"
                      Style [ Display DisplayOptions.Block
                              Width "auto"
                              Margin "auto" ]
                    ]
                br [ ]
                Heading.h3 [ Heading.Modifiers [ Modifier.TextAlignment (Screen.All, TextAlignment.Centered) ] ] [
                    str "Fable REPL"
                ]
                Heading.p [ Heading.IsSubtitle
                            Heading.Is5
                            Heading.Modifiers [ Modifier.TextAlignment (Screen.All, TextAlignment.Centered) ] ] [
                    str "is only available on desktop"
                ]
            ]
        ]
    ]
]

Also, if you use VS Code and something like Rainbow Brackets, it can really help avoid the bracket overload.

isaacabraham commented 4 years ago

Something else to consider (maybe I missed this above) is that GiraffeViewEngine uses the Fable.React style i.e. element attr-list children-list, so just something to think about in terms of people transitioning between the two.

@Zaid-Ajaj makes a good point about Femto; is there any reason why this couldn't be implemented by Fable.React-style components, or is there something inherent about Feliz style that Femto is coupled to?

Zaid-Ajaj commented 4 years ago

is there any reason why this couldn't be implemented by Fable.React-style components, or is there something inherent about Feliz style that Femto is coupled to?

@isaacabraham

Libraries derived from Fable.React could (and should) add Femto metadata but the Fable.React itself couldn't because it has an incompatible mix of react, react-dom and react-native. @MangelMaxime started some work on splitting packages but it was a lot more work than expected and will probably introduce breaking changes.

What this means for libraries derived from Fable.React is that they have to include not just npm metadata of their dependencies but also include compatible versions of react and react-dom because Fable.React doesn't do that for them (unlike Feliz which starts with react and react-dom so that Feliz extensions only add what they need). Unfortunately, I have yet to see any of those libraries take the initiative to better the situation even if the solution has been around for quite some time now.

Regarding formatting: Fantomas is not able yet to format either DSLs in a sensible way and requires some work on making list formatting configurable or have some kind of "Feliz-mode" flag to format the React DSL using nice defaults (which we have to come up with)

l3m commented 4 years ago

@Zaid-Ajaj The issue with React npm dependencies is also more complex as it should be possible to use preact instead of react, without breaking Femto.

MangelMaxime commented 4 years ago

@l3m Indeed, but in this case we could make an addition to Femto to support this case. It remembers me of something so perhaps we have an issue about that in Femto repo (sorry don't have the time to check right now ^^)

Zaid-Ajaj commented 4 years ago

@l3m Preact can be used in place of React regardless of Femto npm metadata because you only need to configure module import aliases as per the guidelines in Switching to Preact when you have an existing React application.

Building a pure Preact binding (100% non-React code) is not the goal of Feliz and should probably be implemented as a standalone library (Feliz.Preact that doesn't depend on Feliz) from which Preact-only bindings will be extended and used in case you want to stay away from preact/compat

l3m commented 4 years ago

@Zaid-Ajaj I'm using preact right now in my Elmish app and it works well. I was just wary about Femto modifying packages.json if it sees a nuget with React deps and I wanted to highlight that. If that's not a problem, all the better.

Also, preact/compat is no longer needed with the latest preact, it's now in core.

fc1943s commented 4 years ago

At the risk is reopening old discussions (sorry!) I just wanted to add my two cents on the indentation debate - this is (more or less) the styling we follow which does allow for nice, consistent identation and following for the Fable.React style.

I'm indenting code in a very similar way from Isaac's, and its pretty readable. I know its hard to compare with a single list way of doing things, but the indenting of the original post could reflect something similar to his code to better compare.

I suppose that with F# 5 and the ability to "open static classes" Feliz will be able to support both Html.div and div syntax depending on if you open or not the Html class.

That would be super cool. Would it be possible to make the Html class static so it could be opened?

Zaid-Ajaj commented 4 years ago

@fc1943s Html, prop and style are all static classes. However, I don't recommend opening them when the feature becomes available in F# 5 because the whole point is having a grouping and an entry point of discovery to the available functions within that static class.

MaxWilsonMS commented 4 years ago

How would opening the static class hinder discovery? You could still type Feliz.Html. when you want to discover what's available.

MangelMaxime commented 4 years ago

@MaxWilsonMS That required additional steps to output the "final" code.

And you would write Feliz.Html.div and then erase Feliz.Html. part which seems strange.

What's good is that people can choose to open the static class or not depending on their preferences. :)

drk-mtr commented 4 years ago

Not sure how useful this will be since I'm echoing what has been said above, but experiences as a complete beginner:

Thanks to all for sharing these libraries!

Shmew commented 4 years ago

Thanks for your input @drk-mtr!

If I was making a custom element, I'd be doing it the Fable.React way for now. I took a look at building things "the Feliz way" and ran away instantly (I'll revisit at some point!).

Yeah, it can appear pretty daunting at first, but it's pretty simple once you get used to it. That being said it is a fair bit of initial cost for the developer when writing libraries. I find this a pretty acceptable trade-off since the vast majority of users aren't going to be writing libraries. Luckily the work itself is pretty simple, and some copy + paste can speed up a lot of the work (or like some of us, writing generators to build the code for us).

I would find it really useful if documentation would more regularly prioritise the "this is how to make a custom property" aspect. I think it's fair to accept that with just a few authors, these libraries are often going to struggle to offer an API surface for everything possible in CSS (for example), so if a beginner gets stuck when they can't set "fontSize" in "em", or can't find "grid-template-rows", or have no idea that they're expected to supply some "Screen.All" argument - it'd good if they knew how to work around it.

Just in case you aren't aware (I couldn't quite tell from this context):

There is a custom method on both the prop and style types.

Everything that accepts ICssUnit can be set like: style.fontSize (length.em 1)

Updating the docs should be easy enough.

I think some "Quality of Life" improvements could be made that start to chip away at some potential concerns... for example when I look at long vertical lists of style props in Feliz I start wondering whether there could be a style.paddingHorizontal and style.paddingVertical. I realise I could make my own helpers, and this isn't the place to make feature requests though, and that there may be a desire to avoid introducing things that aren't standard :)

Anything that helps make the user experience easier is definitely welcome in my book. I'd love to see an issue/pr for any of these things you've noticed!

More generally and probably tangential: The challenge that I consistently hit is around understanding where Elmish world ends and React world starts. This is not really a concern of either library - but since I don't have the foggiest clue about anything React, the fact I could get a Function Component from Fable.React or Feliz confuses me. Of course, learning this is my responsibility - but I thought it was worth mentioning. I'm currently mashing together Elmish and React approaches and I believe this results in all components re-rendering on update - but I don't know the "right" way to do things to avoid this because examples of fully built apps like this are hard to find. Sometimes I get the impression I should learn to make production-grade React apps in Javascript first, then graduate to Fable/Elmish - but that feels like a fairly significant barrier to entry. Disclaimer: I haven't read the Feliz documentation extensively, and it's really good - so this is probably on me!

My experience in the Fable world (pre-Feliz) was I wrote everything as just normal functions for the most part, (like one component for each .fs). The result was I lost a lot of the benefits that the React system provides. Since starting with Feliz I've had to really dive into more on how React works (a good thing imo). Since I've gotten used to it I don't really even give it much conscious thought anymore. Every single function I write that does rendering is a function component.

If you really want to learn how to write React idiomatically I think it's really as simple as restricting yourself to not using Elmish, and only using function components/hooks in your demo app. You can quite easily follow along with anything written in JS out there as the API is almost exactly the same.

Having a quality open-source application written in Feliz is definitely something that would be beneficial, I created an issue for it a while back #67. The closest thing right now is probably the docs app itself, or @cmeeren's Electron Demo.

jkone27 commented 8 months ago

still very relevant discussion also in 2023, even more so with HTMX and the ease of Feliz.ViewEngine, also for pure HTML templating in backend, together with also the other templating approach using string templates withhtml prefix or jsx prefix, but i still think the F# list Feliz approach is just amazing being F# code itself.

i think unification in this area between Fable and Feliz view engine approaches as a single package for both backend and fronted could make sense?

laurentpayot commented 5 months ago

@l3m

For me personally, even though the double list formatting is a pain sometimes, I feel that looking at Fable.React code I get to see the structure of the underlying html better. It's div [.. , not Html.div [.... (etc). It's not a big thing, but my brain seems to have an easier parsing Fable.React.

I’m a little late to the party but I agree that the Html. and prop. prefixes everywhere add some unnecessary noise to the code. You can get rid of the Html. prefix by using open type Html. It is unsafe to do the same with prop because it would replace core F# functions like async. I simply use type P = prop to shorten the prefix.

IMHO, having to read the children prop to see where the children are is more tiring to the eye than when you have two well separated lists. So I use __ as an easily recognizable shortcut for prop.children:

// signature needed to tell the compiler what implementation of prop.children to use
let __: _ seq -> _ = prop.children

All together, I get view elements like the following:

let pageLoader: ReactElement =
    div [
        P.className "center-xy max-xy fixed top left"
        __ [
            progress [ P.className "delayed circle large" ]
        ]
    ]

Just my two cents…