From 4e442bbdcaf53a3be3260ccec681f3496db4cf27 Mon Sep 17 00:00:00 2001 From: "Angel D. Munoz" Date: Wed, 29 Nov 2023 15:07:55 -0600 Subject: [PATCH 01/13] Update the DOM section --- Docs/11 DOM/README.md | 425 ++++++++++++++++++++++++++++++------------ 1 file changed, 301 insertions(+), 124 deletions(-) diff --git a/Docs/11 DOM/README.md b/Docs/11 DOM/README.md index 0b06d5fe..5dc0e4f0 100644 --- a/Docs/11 DOM/README.md +++ b/Docs/11 DOM/README.md @@ -1,160 +1,337 @@ -# DOM +[F# Computation Expressions]: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions +[Adaptive Data]: https://github.com/fsprojects/FSharp.Data.Adaptive +[Working With Blazor]: ./Advanced-features/Working-With-Blazor +[Adaptive Forms]: ./Advanced-features/Adaptive/Form -The most important thing for **Fun.Blazor** is the DSL to build the DOM. -Even before V2, computation expression style DSL is already supported. +# DOM Elements -It is very easy to build and compose the DOM: +Fun.Blazor provides a friendly way to write HTML for web applications. It uses [F# Computation Expressions] to generate a simple yet performant DSL. ```fsharp +let hello name = + div { + id "my-id" + class' "my-class" + $"Hello, {name}" + } -let fragment1 = div { "F=ma" } - -let composed = - div { - style { height 500 } - p { "This is the way!" } - // because below stuff will display something according some conditions, - // so we need use region to isolate it so it will not impact it's parent element/component's sequence numbers for better diff performance. - region { - if conditionIsTrue then - fragment1 - p { "E = MCC" } - } +hello "World!" +``` + +Calling that function would produce a markup like + +```html +
Hello, World!
+``` + +> Some html attributes are reserved keywords in F#, and that's why `'` is added at the end of those names, in this case `class'` instead of `class` + +## Control Flow + +Since you're using F# for your markup code, you have all of the F# arsenal at your disposal that includes `match`, `if`, `function` and even lists of elements but to avoid having problems with mismatch between content in the element rendering you should use `fragment` or `region` these are containerless builders that isolate blazor's node count which can be useful for performance as well as keeping the content concistent. + +### If/Else + +```fsharp + +let element isVisible = + div { + $"The element is: " + region { + if isVisible then + "Visible" + else + "Not Visible" } + } ``` -Also, you can build a shared attributes/style fragment to compose: +### Match ```fsharp -let commonStyle = - css { - cursorPointer - color "green" + +let element kind = + div { + $"The element is: " + region { + match kind with + | Fantastic -> "Fantastic" + | Average -> "Average" + | WellItsSomething -> "Wel... it is something" } + } +``` + +### Lists -let commonAttr = - domAttr { - data 123 +To render lists you can use `fragment` with any seq-like object, for example you can use list/seq comprehentions to render items. +you can also of course prepare the node fragments beforehand and pass the seq to the html.fragment api. + +```fsharp +html.fragment ([ + for item in 0..10 do + li { + key item + $"Item: {item}" } +]) -let sharedButtonAttrs = - button { - style { - commonStyle - // css priority/override is controlled by browser. - // For "color", "red" will be used. - color "red" - } - data 456 - // attribute priority is controlled by blazor core. - // Currently, only the first added attribute will be used when you are trying to add the same attribute. - // That is why I put commonAttr lower than "data 456", - // so the 456 will be used even in commonAttr "data" is 123. - commonAttr - asAttrRenderFragment // Here is the thing to make the magic happen +html.fragment (seq { + for item in 0..10 do + li { + key item + $"Item: {item}" } +}) +``` -let demo = - div { - p { "Below we will have a cool button" } - button { - onclick ignore - sharedButtonAttrs - "Cool" - } - } +> Note: Please note that `key` is very useful to preserve list order between re-renders otherwise you might have unexpected changes in the view when you add/remove items from a list. + +## Attributes + +Fun.Blazor provides out of the box most if not all of the existing HTML attributes in the spec however if you need to set a custom attribute in an element then you can use the `domAttr`` builder that can be used as the following + +```fsharp +section { + domAttr { "my-attribute", "value" } +} ``` -You can also create an extension operation method to create something to reuse. And with this way, it is type safer because it attached to a specific type. It is better to use it for global sharing stuff. As for **asAttrRenderFragment**, it is recommended to use it only locally because what it generates is just **AttrRenderFragment** which can be combined anywhere. +## Events -For CSS, you can do: +Events conform to the standard HTML event names, so you will find them in any element as usual. +handlers can be async or sync depending on your usage but they're often defined as `EventArgs -> unit` or `EventArgs -> Task` ```fsharp -type StyleBuilder with - - [] - member inline _.stack([] comb: CombineKeyValue) = - comb - &&& css { - height "100%" - displayFlex - flexDirectionColumn - alignItemsStretch - overflowHidden - } +button { + "Click Me" + onclick(fun e -> printfn "clicked") +} - [] - member inline _.strench([] comb: CombineKeyValue) = - comb - &&& css { - flex 1 - height "100%" - width "100%" - positionRelative - overflowXHidden - overflowYAuto - } +button { + "Click Me Task" + onclick(fun e -> task { + do! Async.Sleep 1000 + printfn "clicked" + }) +} +``` + +For inputs remember that events provide values as strings, so you have to unbox them + +```fsharp +input { + placeholder "Write Something" + oninput(fun e -> + unbox e.Value |> printfn "New Value: '%s'" + ) +} + +input { + type' "number" + placeholder "Change Number" + oninput(fun e -> + unbox e.Value |> int |> printfn "New Value: '%i'" + ) +} +``` + +> Note: If you're realing with forms you should check out [Adaptive Forms] instead. They can work with more structured objects like records and provide validation abilities. + +# Dynamic content + +> Note: Blazor by default renders the content as static so if you're rendering from the server you won't have any dynamic behavior, for that you need to set server interactivity for the component or use client side blazor (WASM). +> If your project was created with the `fun-wasm` template then you don't have to do anything else for this to work. +> For server interactivity and more information please check the [Working With Blazor] section. + +While much of a website content is static, there are times where you have to match the content based on a value you had previously. +## Adaptive Data -let demo = - div { - style { stack; backgroundColor "blue" } - div { "Header" } - div { - style { strench; backgroundColor "green" } +Fun.Blazor's way to deal with this is by using [Adaptive Data]. Adaptive values behave a lot like excel cells, where a change propagates through the rest of the connected cells. + +This concept has also been used within the javascript ecosystem in recent times often named `signals`. + +The `adaptiview` builder will keep track of adaptive values and re-render the component when a change has been detected, this is provides an efficient rendering mechanism while also providing an immutable way to handle state changes in a view. + +```fsharp +module DynamicViews = + open FSharp.Data.Adaptive + open Fun.Blazor + + let adaptiveNode initialAge = + adaptiview () { + let! age, setAge = cval(10).WithSetter() + + section { + $"Age: {age}" + br + + input { + type' "number" + value age + onchange (fun event -> unbox event.Value |> int |> setAge) } - } + } + } ``` -For DOM element/component, you can do: +## Stores and Observables + +> Note: This requires the [Fun.Blazor.Reactive] package + +### Observables + +> Note: The examples here use the [FSharp.Control.Reactive] package due the functions it provides to help, but it is not required, any `IObservable` works with Fun.Blazor -Without any bindings you can: +If you have observable information around it is quite simple to hook it up with Fun.Blazor by using the `html.watch` API ```fsharp -let demo = - html.blazor (ComponentAttrBuilder() - .Add((fun x -> x.Elevation), 10) - .Add((fun x -> x.Outlined), true) - ) +module ReactiveViews = + open System + open Fun.Blazor + open FSharp.Control.Reactive + + let obs = Observable.interval (TimeSpan.FromSeconds(1.)) + + article { + + h1 { "My View" } + p { + html.watch(obs, (fun num -> fragment { $"Number: {num}" }), 0) + } + } ``` -With auto generated bindings: +### Stores + +Another popular way to handle state changes in the frontend ecosystem is the usage of stores which are containers that keep data around for you. they are very similar to observables but they provide a simpler API to work with. Stores are also useful to bind dynamic attributes ```fsharp -open Fun.Blazor -open Fun.Blazor.Operators -open MudBlazor - -type MudTable'<'T> with - - [] - member this.HeaderAndRow(render: AttrRenderFragment, mappers: (NodeRenderFragment * ('T -> NodeRenderFragment)) seq) = - let headers = mappers |> Seq.map fst - let render = this.HeaderContent(render, html.fragment headers) - this.RowTemplate(render, (fun row -> html.inject (row, (fun () -> mappers |> Seq.map (snd >> fun fn -> fn row) |> html.fragment)))) - - [] - member inline _.withDefaultSettings([] render: AttrRenderFragment) = - render - ==> MudTable'() { - Hover true - FixedHeader true - HorizontalScrollbar true - Breakpoint Breakpoint.None - asAttrRenderFragment // with this feature we can have a better coding experience - } +module ReactiveViews = + open Fun.Blazor + + let store: IStore = new Store(initialAge) + + let view() = + let storeNode = + html.watch(store, (fun num -> fragment { $"Store: {num}" })) + + article { + html.bind("data-current", store) + p { storeNode } + } +``` + +Both Observables and Stores can be converted into adaptive values to have a uniform data interface to work with + +```fsharp + +module AdaptiveViews = + open System + open Fun.Blazor + open FSharp.Control.Reactive + open FSharp.Data.Adaptive + + let observe = Observable.interval (TimeSpan.FromSeconds(1.)) + let store: IStore = new Store(0) + + let adaptiveObs = AVal.ofObservable 0L ignore observe + let adaptiveStore = AVal.ofObservable 0 ignore store.Observable + + let view() = + fragment { + adaptiview () { + let! fromObs = adaptiveObs + let! fromStore = adaptiveStore + fragment { $"Observable: {fromObs} - Store: {fromStore}" } + } + + button { + onclick (fun _ -> store.Publish(store.Current + 10)) + + "Update Store" + } + } +``` + +## Rendering + +Dynamic views re-render when one of the dependencies change and that means the whole node is rendered so you have to keep in mind that whenver you use `html.watch`, `html.bind` or `adaptiview` any of its dependencies may cause a re-render avoid having large trees of information in those places and stay static when possible for performance. -let demo = - MudTable'() { - Height "100%" - Items items - HeaderAndRow [ - MudTh'() { "Name" }, - fun item -> MudTd'() { item.Name } - - MudTh'() { "Age" }, - fun item -> MudTd'() { item.Age } - ] - withDefaultSettings +### Adaptive + +In the case of adaptive values it is easy to track where something will re-render as it is usually surrounded by `adaptiview` anything inside of `adaptiview` will re-render but the outside world will stay static + +```fsharp +// ❌ Avoid +adaptiview() { + let! content = myAValue + + $"Hello static string" // will re-render + div { "Hello static node" } // will re-render + + p { // will re-render + div { // will re-render + $"Hello dynamic content {content}"// will re-render + } + } +} +``` + +```fsharp +// ✅ Prefer +fragment { + $"Hello static string" // Stays static + div { "Hello static node" } // Stays static + + p { // Stays static + div { // Stays static + adaptiview() { + let! content = myAValue + fragment { $"Hello dynamic content {content}" } // will re-render + } } -``` \ No newline at end of file + } +} +``` + +### Stores and Observables + +In a similar fashion to adaptive values, whenever you see a `html.watch` call it means that the whole node will re-render when the observable/store value changes. + +```fsharp +// ❌ Avoid +html.watch( + obs, + (fun value -> fragment { + $"Hello static string" // will re-render + div { "Hello static node" } // will re-render + + p { // will re-render + div { // will re-render + $"Hello dynamic content {value}"// will re-render + } + } + }), +0) +``` + +```fsharp +// ✅ Prefer +fragment { + $"Hello static string" // will stay static + div { "Hello static node" } // will stay static + + p { // will stay static + div { // will stay static + "Hello dynamic content " // will stay static + html.watch( + obs, + (fun value -> fragment { $"{value}" }), //will re-render + 0 + ) + } + } +} +``` From b357833ecd9d14fc37742ed2a7f621a1f6ed8b08 Mon Sep 17 00:00:00 2001 From: "Angel D. Munoz" Date: Wed, 29 Nov 2023 15:08:02 -0600 Subject: [PATCH 02/13] Update the Style section --- Docs/12 Style/README.md | 193 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 187 insertions(+), 6 deletions(-) diff --git a/Docs/12 Style/README.md b/Docs/12 Style/README.md index 1e59ee9a..58aa9baa 100644 --- a/Docs/12 Style/README.md +++ b/Docs/12 Style/README.md @@ -1,15 +1,196 @@ -# Style +[Fun.Css]: https://github.com/slaveOftime/Fun.Css +[rule's Specificity]: https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity -This was created by [Fun.Css](https://github.com/slaveOftime/Fun.Css). It can be used to build CSS rules or inline styles. +# Styling -Since it uses computation expression, you can customize its operations as well: +Adding styles is common in every web application, Fun.Blazor provides out of the box support for inline css and also there's the [Fun.Css] package that provides support for style tags (using classes and rulesets). + +## Inline Styles + +Adding inline styles to elements is very straight forward + +```fsharp +div { + style { + color "red" + } +} +``` + +Would produce + +```html +
+``` + +If you like to pursue inline styles you can also share those styles, for that we have to add a reference to [Fun.Css] in our project and then we can start sharing styles: + +```fsharp +module SharedStyles = + let ClickableGreen = + css { + cursorPointer + color "green" + } + +let styledDiv = + div { + style { + SharedStyles.ClickableGreen + fontSize 16 + } + } +``` + +> Note: The normal styling rules still apply, if we supply a `color "red"` attribute in our button, the browser will pick up that instead of our shared style. Keep in mind however that it also means you will have duplicated css properties in inline styles. + +## Style Sheets + +To create style tags you can use the `ruleset` builder, this will produce a css rule that can be applied to any element within the scope of the style tag (e.g. when it loads) + +```fsharp +module Shared = + let Page = + ruleset ".page" { + margin 0 + padding "1em" + displayFlex + flexDirectionColumn + height "100vh" + } + + let Title = + ruleset ".title" { fontSize "calc(10px + 3vmin)" } + + let Text = + ruleset ".text" { fontSize "calc(10px + 1vmin)" } + +``` + +With those rules in mind we can then add a "home page" + +```fsharp +let HomePage = + article { + class' "page home" + h1 { class' "title"; "This is the title" } + + section { + p { class' "text"; "This is some text" } + p { "This is another text" } + } + + styleElt { + ruleset ".home" { + fontSize "medium" + boxShadow "5px 5px 0.5em rgba(0, 0, 0, 0.5)" + } + Shared.Page + Shared.Title + Shared.Text + } + } +``` + +That would produce something like + +```html +
+

This is the title

+
+

This is some text

+

This is another text

+
+ +
+``` + +> Note: Keep in mind that these styles are loaded in style tags along their declaring elements, so if you have styles already loaded you could have rules clashing and overriding depending on the [rule's specificity]. + +## Sharing Inline Styles Globally + +There are some cases where you'd like yo have certain styles available for any element you can also create an extension operation method to create something to reuse. + +We'll add a couple of "stacks" to our styles using flexbox, for this we have to extend the style builder in Fun.Blazor. + +```fsharp +[] +module StyleExtensions = + open Fun.Blazor.Internal + open Fun.Css.Internal + + type StyleBuilder with + + [] + member inline _.VStack([] comb: CombineKeyValue) = + comb + &&& css { + displayFlex + flexDirectionColumn + } + + [] + member inline _.HStack([] comb: CombineKeyValue) = comb &&& css { displayFlex } + + [] + member inline this.EvenHStack([] comb: CombineKeyValue) = + this.HStack comb &&& css { custom "justify-content" "space-evenly" } + +``` + +> Note: `&&&` is a custom operator provided by Fun.Css so we don't have to manually combine those styles ourselves. + +Once we have defined our extension members on the style builder, we can use those in the following way: + +```fsharp +let MyItems = + ul { + // display: flex; flex-direction: column; + style { VStack } + + // content + } +let MyItems2 = + ul { + // display: flex; + style { HStack } + + // content + } +let MyItems3 = + ul { + // display: flex; justify-content: space-evenly; + style { EvenHStack } + // content + } +``` + +### Other Examples: {{InlineStyleDemo}} -Additionally, you can create CSS rules with it: +Using CSS rules: {{CssRuleDemo}} -Even simple keyframes can be built: +Using keyframes: -{{KeyFramesDemo}} \ No newline at end of file +{{KeyFramesDemo}} From 448641bb420337f26aecb610070e9e32a5079a7e Mon Sep 17 00:00:00 2001 From: "Angel D. Munoz" Date: Wed, 29 Nov 2023 15:08:14 -0600 Subject: [PATCH 03/13] Move blazor content to its own document --- .../44 Working With Blazor/README.md | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 Docs/40 Advanced/44 Working With Blazor/README.md diff --git a/Docs/40 Advanced/44 Working With Blazor/README.md b/Docs/40 Advanced/44 Working With Blazor/README.md new file mode 100644 index 00000000..ecc8e5f0 --- /dev/null +++ b/Docs/40 Advanced/44 Working With Blazor/README.md @@ -0,0 +1,132 @@ +[Fun.Blazor.Cli]: ./Tooling/Code-Generation +[Code Generation]: ./Tooling/Code-Generation +[Blazor Rendering Modes]: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-8.0 + +# Working with Blazor + +Using Blazor features with almost "raw" blazor elements is possible in Fun.Blazor + +Most of the ways to render Blazor components in Fun.Blazor is with the `html.blazor` API it provides of several overloads you can use to render elements as required. + +### Add Attributes + +To add attributes to a blazor component you can use the `domAttr` builder + +```fsharp +html.blazor ( + domAttr { + class' "my-component" + data ("some-dalue", initialValue) + } +) +``` + +### Add Members + +When you want to bind to particular members in a component you can do so by using the `ComponentAttrBuilder()` builder, this provides a strongly typed access to the component and the properties it has exposed to bind for example: + +```fsharp +html.blazor ( + ComponentAttrBuilder() + .Add((fun x -> x.Value), "Some value") + .Add((fun x -> x.OnValueChange), EventCallback(null, Action (fun v -> printfn $"{v}"))) +) +``` + +> Note: Element attributes are not the same as members, element attributes are a concept tied to HTML (e.g. class styles or inline styles), members are part of the object itself this distinction also applies to javascript in the attributes vs properties situation. + +### Server/Wasm/Auto Rendering + +In .NET8 a few rendering modes were added into blazor you can also use these to ensure where do you need your blazor elements to render + +```fsharp +html.blazor(RenderMode.InteractiveServer) + +html.blazor(RenderMode.InteractiveAuto) + +html.blazor(RenderMode.InteractiveWebAssembly) +``` + +For more information about what are the effects please visit the Microsoft documentation for [Blazor Rendering Modes]. + +## Blazor Components + +If you're working with a place where there are C# and F# teams you may want to work with plain Blazor components. + +Thankfully these are very simple to define as they are just a class like the following: + +```fsharp +type MyComponent() = + inherit FunComponent() // This is required + + [] + member val Logger: ILogger = Unchecked.defaultof<_> with get, set + + [] + member val Value = "" with get, set + + [] + member val OnValueChangeCb: EventCallback = Unchecked.defaultof<_> with get, set + + override this.Render() = + div { + $"Hello {this.Value}!" + + textarea { + type' "text" + value this.Value + + oninput + (fun e -> + let value = unbox e.Value + this.Logger.LogDebug("Value Changed: {OldValue} -> {NewValue}", this.Value, value) + this.OnValueChangeCb.InvokeAsync value + ) + } + } +``` + +You can define `Parameters`, `CascadingParameters`, `EventCallbacks`, also use attributes like `Inject`, `Route`, `StreamRendering`, and most if not all of what you'd expect in your blazor components in the C# counterpart. + +You can use the Fun.Blazor DSL to work with your markup and that includes dynamic content with adaptive data. + +## Blazor Bindings + +Sometimes you'd like to use third party libraries in Fun.Blazor you can write manual bindings or use the [Fun.Blazor.Cli] tool to generate them for you. + +For the manual bindings we can use `ComponentAttrBuilder()` like the following example: + +```fsharp +// This is an existing component it can be defined in C# or F# +type MyComponent with + + static member create(value, onValueChanged) = + html.blazor ( + ComponentAttrBuilder() + .Add((fun x -> x.Value), value) + .Add((fun x -> x.OnValueChangeCb), EventCallback(null, Action onValueChanged)) + ) +``` + +With our `create` extension we can now use it in a seamless way with the rest of our Fun.Blazor markup + +```fsharp +module Home = + open FSharp.Data.Adaptive + open Fun.Blazor + + let view() = + article { + h1 { "This is a title" } + + adaptiview() { + let! value, setValue = cval("").WithSetter() + + MyComponent.create(value, setValue) + + $"the value is: {value.ToUpperInvariant()}" + } + } +``` + +This can be a tedious process however [Fun.Blazor.Cli] provides an automated tool to generate these, please visit the [Code Generation] section for more information on how to generate new bindings. From cb03664b56ee3c6dfd667afcede42519849db771 Mon Sep 17 00:00:00 2001 From: "Angel D. Munoz" Date: Wed, 29 Nov 2023 16:55:27 -0600 Subject: [PATCH 04/13] address feedback comments --- Docs/11 DOM/README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Docs/11 DOM/README.md b/Docs/11 DOM/README.md index 5dc0e4f0..3f4410c5 100644 --- a/Docs/11 DOM/README.md +++ b/Docs/11 DOM/README.md @@ -104,16 +104,16 @@ handlers can be async or sync depending on your usage but they're often defined ```fsharp button { - "Click Me" onclick(fun e -> printfn "clicked") + "Click Me" } button { - "Click Me Task" onclick(fun e -> task { do! Async.Sleep 1000 printfn "clicked" }) + "Click Me Task" } ``` @@ -159,7 +159,7 @@ module DynamicViews = open FSharp.Data.Adaptive open Fun.Blazor - let adaptiveNode initialAge = + let adaptiveNode = adaptiview () { let! age, setAge = cval(10).WithSetter() @@ -218,7 +218,6 @@ module ReactiveViews = html.watch(store, (fun num -> fragment { $"Store: {num}" })) article { - html.bind("data-current", store) p { storeNode } } ``` From 50612a21a9801581e5c8259a89b16308594f2d1a Mon Sep 17 00:00:00 2001 From: "Angel D. Munoz" Date: Wed, 29 Nov 2023 17:15:41 -0600 Subject: [PATCH 05/13] Add attribute ordering note --- Docs/11 DOM/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Docs/11 DOM/README.md b/Docs/11 DOM/README.md index 3f4410c5..2b439d4f 100644 --- a/Docs/11 DOM/README.md +++ b/Docs/11 DOM/README.md @@ -26,6 +26,8 @@ Calling that function would produce a markup like > Some html attributes are reserved keywords in F#, and that's why `'` is added at the end of those names, in this case `class'` instead of `class` +> Note: Attributes must be placed before any other elements, like strings or other nodes + ## Control Flow Since you're using F# for your markup code, you have all of the F# arsenal at your disposal that includes `match`, `if`, `function` and even lists of elements but to avoid having problems with mismatch between content in the element rendering you should use `fragment` or `region` these are containerless builders that isolate blazor's node count which can be useful for performance as well as keeping the content concistent. From ad8c7414b84d5ef4d2da8e90470e0b4a6ec7ccf0 Mon Sep 17 00:00:00 2001 From: "Angel D. Munoz" Date: Wed, 29 Nov 2023 17:18:08 -0600 Subject: [PATCH 06/13] Move dynamic content to its own section --- Docs/11 DOM/README.md | 197 ------------------------------------------ 1 file changed, 197 deletions(-) diff --git a/Docs/11 DOM/README.md b/Docs/11 DOM/README.md index 2b439d4f..9afaa53d 100644 --- a/Docs/11 DOM/README.md +++ b/Docs/11 DOM/README.md @@ -139,200 +139,3 @@ input { ``` > Note: If you're realing with forms you should check out [Adaptive Forms] instead. They can work with more structured objects like records and provide validation abilities. - -# Dynamic content - -> Note: Blazor by default renders the content as static so if you're rendering from the server you won't have any dynamic behavior, for that you need to set server interactivity for the component or use client side blazor (WASM). -> If your project was created with the `fun-wasm` template then you don't have to do anything else for this to work. -> For server interactivity and more information please check the [Working With Blazor] section. - -While much of a website content is static, there are times where you have to match the content based on a value you had previously. - -## Adaptive Data - -Fun.Blazor's way to deal with this is by using [Adaptive Data]. Adaptive values behave a lot like excel cells, where a change propagates through the rest of the connected cells. - -This concept has also been used within the javascript ecosystem in recent times often named `signals`. - -The `adaptiview` builder will keep track of adaptive values and re-render the component when a change has been detected, this is provides an efficient rendering mechanism while also providing an immutable way to handle state changes in a view. - -```fsharp -module DynamicViews = - open FSharp.Data.Adaptive - open Fun.Blazor - - let adaptiveNode = - adaptiview () { - let! age, setAge = cval(10).WithSetter() - - section { - $"Age: {age}" - br - - input { - type' "number" - value age - onchange (fun event -> unbox event.Value |> int |> setAge) - } - } - } -``` - -## Stores and Observables - -> Note: This requires the [Fun.Blazor.Reactive] package - -### Observables - -> Note: The examples here use the [FSharp.Control.Reactive] package due the functions it provides to help, but it is not required, any `IObservable` works with Fun.Blazor - -If you have observable information around it is quite simple to hook it up with Fun.Blazor by using the `html.watch` API - -```fsharp -module ReactiveViews = - open System - open Fun.Blazor - open FSharp.Control.Reactive - - let obs = Observable.interval (TimeSpan.FromSeconds(1.)) - - article { - - h1 { "My View" } - p { - html.watch(obs, (fun num -> fragment { $"Number: {num}" }), 0) - } - } -``` - -### Stores - -Another popular way to handle state changes in the frontend ecosystem is the usage of stores which are containers that keep data around for you. they are very similar to observables but they provide a simpler API to work with. Stores are also useful to bind dynamic attributes - -```fsharp -module ReactiveViews = - open Fun.Blazor - - let store: IStore = new Store(initialAge) - - let view() = - let storeNode = - html.watch(store, (fun num -> fragment { $"Store: {num}" })) - - article { - p { storeNode } - } -``` - -Both Observables and Stores can be converted into adaptive values to have a uniform data interface to work with - -```fsharp - -module AdaptiveViews = - open System - open Fun.Blazor - open FSharp.Control.Reactive - open FSharp.Data.Adaptive - - let observe = Observable.interval (TimeSpan.FromSeconds(1.)) - let store: IStore = new Store(0) - - let adaptiveObs = AVal.ofObservable 0L ignore observe - let adaptiveStore = AVal.ofObservable 0 ignore store.Observable - - let view() = - fragment { - adaptiview () { - let! fromObs = adaptiveObs - let! fromStore = adaptiveStore - fragment { $"Observable: {fromObs} - Store: {fromStore}" } - } - - button { - onclick (fun _ -> store.Publish(store.Current + 10)) - - "Update Store" - } - } -``` - -## Rendering - -Dynamic views re-render when one of the dependencies change and that means the whole node is rendered so you have to keep in mind that whenver you use `html.watch`, `html.bind` or `adaptiview` any of its dependencies may cause a re-render avoid having large trees of information in those places and stay static when possible for performance. - -### Adaptive - -In the case of adaptive values it is easy to track where something will re-render as it is usually surrounded by `adaptiview` anything inside of `adaptiview` will re-render but the outside world will stay static - -```fsharp -// ❌ Avoid -adaptiview() { - let! content = myAValue - - $"Hello static string" // will re-render - div { "Hello static node" } // will re-render - - p { // will re-render - div { // will re-render - $"Hello dynamic content {content}"// will re-render - } - } -} -``` - -```fsharp -// ✅ Prefer -fragment { - $"Hello static string" // Stays static - div { "Hello static node" } // Stays static - - p { // Stays static - div { // Stays static - adaptiview() { - let! content = myAValue - fragment { $"Hello dynamic content {content}" } // will re-render - } - } - } -} -``` - -### Stores and Observables - -In a similar fashion to adaptive values, whenever you see a `html.watch` call it means that the whole node will re-render when the observable/store value changes. - -```fsharp -// ❌ Avoid -html.watch( - obs, - (fun value -> fragment { - $"Hello static string" // will re-render - div { "Hello static node" } // will re-render - - p { // will re-render - div { // will re-render - $"Hello dynamic content {value}"// will re-render - } - } - }), -0) -``` - -```fsharp -// ✅ Prefer -fragment { - $"Hello static string" // will stay static - div { "Hello static node" } // will stay static - - p { // will stay static - div { // will stay static - "Hello dynamic content " // will stay static - html.watch( - obs, - (fun value -> fragment { $"{value}" }), //will re-render - 0 - ) - } - } -} -``` From 91e6a683d9faa6c7c877b13ea6d5a2c5ca85d127 Mon Sep 17 00:00:00 2001 From: "Angel D. Munoz" Date: Wed, 29 Nov 2023 17:18:34 -0600 Subject: [PATCH 07/13] Add new section about interactive nodes --- Docs/13 Interactive Nodes/README.md | 199 ++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 Docs/13 Interactive Nodes/README.md diff --git a/Docs/13 Interactive Nodes/README.md b/Docs/13 Interactive Nodes/README.md new file mode 100644 index 00000000..6ef467d2 --- /dev/null +++ b/Docs/13 Interactive Nodes/README.md @@ -0,0 +1,199 @@ +[Adaptive Data]: https://github.com/fsprojects/FSharp.Data.Adaptive +[Working With Blazor]: ./Advanced-features/Working-With-Blazor + +# Interactive Nodes + +> Note: Blazor by default renders the content as static so if you're rendering from the server you won't have any dynamic behavior, for that you need to set server interactivity for the component or use client side blazor (WASM). +> If your project was created with the `fun-wasm` template then you don't have to do anything else for this to work. +> For server interactivity and more information please check the [Working With Blazor] section. + +While much of a website content is static, there are times where you have to match the content based on a value you had previously. + +## Adaptive Data + +Fun.Blazor's way to deal with this is by using [Adaptive Data]. Adaptive values behave a lot like excel cells, where a change propagates through the rest of the connected cells. + +This concept has also been used within the javascript ecosystem in recent times often named `signals`. + +The `adaptiview` builder will keep track of adaptive values and re-render the component when a change has been detected, this is provides an efficient rendering mechanism while also providing an immutable way to handle state changes in a view. + +```fsharp +module DynamicViews = + open FSharp.Data.Adaptive + open Fun.Blazor + + let adaptiveNode = + adaptiview () { + let! age, setAge = cval(10).WithSetter() + + section { + $"Age: {age}" + br + + input { + type' "number" + value age + onchange (fun event -> unbox event.Value |> int |> setAge) + } + } + } +``` + +## Stores and Observables + +> Note: This requires the [Fun.Blazor.Reactive] package + +### Observables + +> Note: The examples here use the [FSharp.Control.Reactive] package due the functions it provides to help, but it is not required, any `IObservable` works with Fun.Blazor + +If you have observable information around it is quite simple to hook it up with Fun.Blazor by using the `html.watch` API + +```fsharp +module ReactiveViews = + open System + open Fun.Blazor + open FSharp.Control.Reactive + + let obs = Observable.interval (TimeSpan.FromSeconds(1.)) + + article { + + h1 { "My View" } + p { + html.watch(obs, (fun num -> fragment { $"Number: {num}" }), 0) + } + } +``` + +### Stores + +Another popular way to handle state changes in the frontend ecosystem is the usage of stores which are containers that keep data around for you. they are very similar to observables but they provide a simpler API to work with. Stores are also useful to bind dynamic attributes + +```fsharp +module ReactiveViews = + open Fun.Blazor + + let store: IStore = new Store(initialAge) + + let view() = + let storeNode = + html.watch(store, (fun num -> fragment { $"Store: {num}" })) + + article { + p { storeNode } + } +``` + +Both Observables and Stores can be converted into adaptive values to have a uniform data interface to work with + +```fsharp + +module AdaptiveViews = + open System + open Fun.Blazor + open FSharp.Control.Reactive + open FSharp.Data.Adaptive + + let observe = Observable.interval (TimeSpan.FromSeconds(1.)) + let store: IStore = new Store(0) + + let adaptiveObs = AVal.ofObservable 0L ignore observe + let adaptiveStore = AVal.ofObservable 0 ignore store.Observable + + let view() = + fragment { + adaptiview () { + let! fromObs = adaptiveObs + let! fromStore = adaptiveStore + fragment { $"Observable: {fromObs} - Store: {fromStore}" } + } + + button { + onclick (fun _ -> store.Publish(store.Current + 10)) + + "Update Store" + } + } +``` + +## Rendering + +Dynamic views re-render when one of the dependencies change and that means the whole node is rendered so you have to keep in mind that whenver you use `html.watch`, `html.bind` or `adaptiview` any of its dependencies may cause a re-render avoid having large trees of information in those places and stay static when possible for performance. + +### Adaptive + +In the case of adaptive values it is easy to track where something will re-render as it is usually surrounded by `adaptiview` anything inside of `adaptiview` will re-render but the outside world will stay static + +```fsharp +// ❌ Avoid +adaptiview() { + let! content = myAValue + + $"Hello static string" // will re-render + div { "Hello static node" } // will re-render + + p { // will re-render + div { // will re-render + $"Hello dynamic content {content}"// will re-render + } + } +} +``` + +```fsharp +// ✅ Prefer +fragment { + $"Hello static string" // Stays static + div { "Hello static node" } // Stays static + + p { // Stays static + div { // Stays static + adaptiview() { + let! content = myAValue + fragment { $"Hello dynamic content {content}" } // will re-render + } + } + } +} +``` + +### Stores and Observables + +In a similar fashion to adaptive values, whenever you see a `html.watch` call it means that the whole node will re-render when the observable/store value changes. + +```fsharp +// ❌ Avoid +html.watch( + obs, + (fun value -> fragment { + $"Hello static string" // will re-render + div { "Hello static node" } // will re-render + + p { // will re-render + div { // will re-render + $"Hello dynamic content {value}"// will re-render + } + } + }), +0) +``` + +```fsharp +// ✅ Prefer +fragment { + $"Hello static string" // will stay static + div { "Hello static node" } // will stay static + + p { // will stay static + div { // will stay static + "Hello dynamic content " // will stay static + html.watch( + obs, + (fun value -> fragment { $"{value}" }), //will re-render + 0 + ) + } + } +} +``` From de3259c2b5f88ff9ab58b3aca24568e58140d0cc Mon Sep 17 00:00:00 2001 From: "Angel D. Munoz" Date: Wed, 29 Nov 2023 17:24:01 -0600 Subject: [PATCH 08/13] add missing links --- Docs/13 Interactive Nodes/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Docs/13 Interactive Nodes/README.md b/Docs/13 Interactive Nodes/README.md index 6ef467d2..3977ad84 100644 --- a/Docs/13 Interactive Nodes/README.md +++ b/Docs/13 Interactive Nodes/README.md @@ -1,5 +1,7 @@ [Adaptive Data]: https://github.com/fsprojects/FSharp.Data.Adaptive [Working With Blazor]: ./Advanced-features/Working-With-Blazor +[Fun.Blazor.Reactive]: https://github.com/slaveOftime/Fun.Blazor +[FSharp.Control.Reactive]: http://fsprojects.github.io/FSharp.Control.Reactive/index.html # Interactive Nodes @@ -11,7 +13,7 @@ While much of a website content is static, there are times where you have to mat ## Adaptive Data -Fun.Blazor's way to deal with this is by using [Adaptive Data]. Adaptive values behave a lot like excel cells, where a change propagates through the rest of the connected cells. +Fun.Blazor's way to deal with this is by using [Adaptive Data] which is included out of the box. Adaptive values behave a lot like excel cells, where a change propagates through the rest of the connected cells. This concept has also been used within the javascript ecosystem in recent times often named `signals`. From 82b96337b101cb8f50945aa1b596053c368e9468 Mon Sep 17 00:00:00 2001 From: "Angel D. Munoz" Date: Wed, 29 Nov 2023 22:11:33 -0600 Subject: [PATCH 09/13] add Classes and Functions document --- Docs/14 Classes and Functions/README.md | 131 ++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 Docs/14 Classes and Functions/README.md diff --git a/Docs/14 Classes and Functions/README.md b/Docs/14 Classes and Functions/README.md new file mode 100644 index 00000000..f8442912 --- /dev/null +++ b/Docs/14 Classes and Functions/README.md @@ -0,0 +1,131 @@ +[Interactive Nodes]: ./Interactive-Nodes +[Hooks]: ./Advanced-features/Hook + +# Classes vs Functions + +Fun.Blazor is very flexible and allows you to choose between classes and functions as you need. + +For the most part you can use functions as much as you'd like but there are situations wher classes may come in handy (like writing blazor components for other teams). + +## Functions + +Functions are executed once and they are not re-executed unless the parent re-renders + +```fsharp +let staticGreeting name = + p { $"Hello there, {name}!" } +``` + +This function given the same parameter will always output the same p tag with the same content and it will never change again, the same goes for functions that enclose mutable values, for example the following function will not re-render + +```fsharp +let staticGreeting()= + let mutable counter = 0 + + button { + onclick (fun _ -> counter <- counter + 1) + $"Click Me: {counter}" + } +``` + +This is because the change detection mechanics happen at the component level, that means class inheriting ComponentBase which in the case of Fun.Blazor is `FunComponent`. + +However if you want to work with dynamic data in your functions, you can use `Adaptive` values, `Stores`, or `Observables` which you can read further in [Interactive Nodes]. A brief example with adaptive values would be: + +```fsharp + +let staticChild name (currentName: string cval) = + button { + // trigger currentName changes + onclick(fun _ -> currentname.Publish name) + $"Click {name}!" + } + +let staticParent () = + let currentName = cval("Nobody") + fragment { + // doesn't re-render + h1 { $"Initial Name: {currentName.Value}" } + + // render on currentName changes + adaptiview() { + let! currentName = currentName + h1 { $"Hello, {currentName}!" } + } + + staticChild "Peter" currentName + staticChild "Frank" currentName + } +``` + +This reduces the need to have classes for most of the codebase, you can combine dynamic data with Fun.Blazor's DSL to go as functional as you need. + +### Benefits and drawbacks + +The benefits are: + +- Simpler to test +- Easier to reason about +- Composable + +They are often the preferred way to go for most F# users. + +The downsides are: + +- Don't have access to aspnet's default DI container +- Don't play well raw blazor +- They need to be wrapped in components in order for C# consumers to use + +In short the closer you want to get to Blazor, the least functions work nicely with it. + +However, most of the time a function is the simple yet correct choice. + +## Classes + +Blazor by default re-renders in certain conditions like after an event handler is executed, changing parameters/cascading parameters and when calling `this.StateHasChanged()` so it plays well with mutable values even in F#. + +A simple counter example would be the following: + +```fsharp +type Counter() = + inherit FunComponent() + + let mutable counter = 0 + [] + member val Logger: ILogger = Unchecked.defaultof<_> + + override this.Render() = + fragment { + h1 { $"Count: {counter}" } + + button { + onclick (fun _ -> + let newCount = counter + 1 + Logger.LogDebug("Old Value: {count} - New Value: {newCount}", counter, newCount) + counter <- newCount + ) + "Increment" + } + } +``` + +In this case we didn't add any dynamic values like Adaptive data, Stores or Observables and our counter is re-rendered and works as expected, blazor applies a diffing algorithm to know that it only needs to re-render the `h1` tag. you can also however use dynamic data if required. + +### Benefits and drawbacks + +The benefits are: + +- Offer full decorator support `Parameter`, `Cascading Parameter`, `Route`, `Inject` +- Has direct access to the DI container in aspnet +- Consumption is transparent for C# consumers +- Plays well with the blazor framework under the hood. + +The drawbacks are: + +- Re-rendering requires diffing which can be expensive if you're not using dynamic data +- They require manual bindings to play well with the Fun.Blazor DSL +- Verbose + +In general, classes can be used to orchestrate dependency injection, to use plain blazor features seamlessly and in general to keep as close as blazor as possible. + +Functions most of the time can overcome the benefits of classes when you use `html.comp` functions (also called `html.inject`) which you can read more about in [Hooks] and [Dependency Injection](<./Advanced-features/Dependency-injection-(DI)>) From 04e96c1744abd416a1a1012973be1d4a6082c969 Mon Sep 17 00:00:00 2001 From: "Angel D. Munoz" Date: Thu, 30 Nov 2023 21:21:40 -0600 Subject: [PATCH 10/13] - address some of the feedback - move links to the bottom of the file --- Docs/11 DOM/README.md | 59 +++++++++++++++++-------- Docs/12 Style/README.md | 6 +-- Docs/13 Interactive Nodes/README.md | 12 ++--- Docs/14 Classes and Functions/README.md | 9 ++-- 4 files changed, 54 insertions(+), 32 deletions(-) diff --git a/Docs/11 DOM/README.md b/Docs/11 DOM/README.md index 9afaa53d..6d638d45 100644 --- a/Docs/11 DOM/README.md +++ b/Docs/11 DOM/README.md @@ -1,9 +1,4 @@ -[F# Computation Expressions]: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions -[Adaptive Data]: https://github.com/fsprojects/FSharp.Data.Adaptive -[Working With Blazor]: ./Advanced-features/Working-With-Blazor -[Adaptive Forms]: ./Advanced-features/Adaptive/Form - -# DOM Elements +# DOM Fun.Blazor provides a friendly way to write HTML for web applications. It uses [F# Computation Expressions] to generate a simple yet performant DSL. @@ -66,39 +61,60 @@ let element kind = ### Lists -To render lists you can use `fragment` with any seq-like object, for example you can use list/seq comprehentions to render items. -you can also of course prepare the node fragments beforehand and pass the seq to the html.fragment api. +To render lists you can use `for item in items do` ```fsharp -html.fragment ([ +ul { + h3 { "Some title." } for item in 0..10 do li { key item $"Item: {item}" } -]) -html.fragment (seq { - for item in 0..10 do - li { - key item - $"Item: {item}" - } -}) + // or also if you have an existing list of nodes + +} ``` > Note: Please note that `key` is very useful to preserve list order between re-renders otherwise you might have unexpected changes in the view when you add/remove items from a list. ## Attributes -Fun.Blazor provides out of the box most if not all of the existing HTML attributes in the spec however if you need to set a custom attribute in an element then you can use the `domAttr`` builder that can be used as the following +Fun.Blazor provides out of the box most if not all of the existing HTML attributes in the spec however if you need to set a custom attribute in an element then you can provide the builder with a string tuple. ```fsharp section { - domAttr { "my-attribute", "value" } + "my-attribute", "value" } ``` +### Shared attributes + +If you'd like to share attributes between different elements you can use the `domAttr` + +```fsharp +module SharedAttrs = + let classAndData = + domAttr { + class' "has-data" + data("my-data", "123") + } + +let someNode() = + div { + SharedAttrs.classAndData + "Some Node" + } + +let otherNode() = + div { + SharedAttrs.classAndData + "Other Node" + } + +``` + ## Events Events conform to the standard HTML event names, so you will find them in any element as usual. @@ -139,3 +155,8 @@ input { ``` > Note: If you're realing with forms you should check out [Adaptive Forms] instead. They can work with more structured objects like records and provide validation abilities. + +[F# Computation Expressions]: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions +[Adaptive Data]: https://github.com/fsprojects/FSharp.Data.Adaptive +[Working With Blazor]: ./Advanced-features/Working-With-Blazor +[Adaptive Forms]: ./Advanced-features/Adaptive/Form diff --git a/Docs/12 Style/README.md b/Docs/12 Style/README.md index 58aa9baa..d1ad9eab 100644 --- a/Docs/12 Style/README.md +++ b/Docs/12 Style/README.md @@ -1,6 +1,3 @@ -[Fun.Css]: https://github.com/slaveOftime/Fun.Css -[rule's Specificity]: https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity - # Styling Adding styles is common in every web application, Fun.Blazor provides out of the box support for inline css and also there's the [Fun.Css] package that provides support for style tags (using classes and rulesets). @@ -194,3 +191,6 @@ Using CSS rules: Using keyframes: {{KeyFramesDemo}} + +[Fun.Css]: https://github.com/slaveOftime/Fun.Css +[rule's Specificity]: https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity diff --git a/Docs/13 Interactive Nodes/README.md b/Docs/13 Interactive Nodes/README.md index 3977ad84..443d0a92 100644 --- a/Docs/13 Interactive Nodes/README.md +++ b/Docs/13 Interactive Nodes/README.md @@ -1,8 +1,3 @@ -[Adaptive Data]: https://github.com/fsprojects/FSharp.Data.Adaptive -[Working With Blazor]: ./Advanced-features/Working-With-Blazor -[Fun.Blazor.Reactive]: https://github.com/slaveOftime/Fun.Blazor -[FSharp.Control.Reactive]: http://fsprojects.github.io/FSharp.Control.Reactive/index.html - # Interactive Nodes > Note: Blazor by default renders the content as static so if you're rendering from the server you won't have any dynamic behavior, for that you need to set server interactivity for the component or use client side blazor (WASM). @@ -121,7 +116,7 @@ module AdaptiveViews = ## Rendering -Dynamic views re-render when one of the dependencies change and that means the whole node is rendered so you have to keep in mind that whenver you use `html.watch`, `html.bind` or `adaptiview` any of its dependencies may cause a re-render avoid having large trees of information in those places and stay static when possible for performance. +Dynamic views re-render when one of the dependencies change and that means the whole node is rendered so you have to keep in mind that whenver you use `html.watch`, or `adaptiview` any of its dependencies may cause a re-render avoid having large trees of information in those places and stay static when possible for performance. ### Adaptive @@ -199,3 +194,8 @@ fragment { } } ``` + +[Adaptive Data]: https://github.com/fsprojects/FSharp.Data.Adaptive +[Working With Blazor]: ./Advanced-features/Working-With-Blazor +[Fun.Blazor.Reactive]: https://github.com/slaveOftime/Fun.Blazor +[FSharp.Control.Reactive]: http://fsprojects.github.io/FSharp.Control.Reactive/index.html diff --git a/Docs/14 Classes and Functions/README.md b/Docs/14 Classes and Functions/README.md index f8442912..5e462f18 100644 --- a/Docs/14 Classes and Functions/README.md +++ b/Docs/14 Classes and Functions/README.md @@ -1,6 +1,3 @@ -[Interactive Nodes]: ./Interactive-Nodes -[Hooks]: ./Advanced-features/Hook - # Classes vs Functions Fun.Blazor is very flexible and allows you to choose between classes and functions as you need. @@ -29,6 +26,7 @@ let staticGreeting()= ``` This is because the change detection mechanics happen at the component level, that means class inheriting ComponentBase which in the case of Fun.Blazor is `FunComponent`. +However If change the declaration from a function (`let staticGreeting () =`) to a let bound value (`let staticGreeting =`) , and use it in a context which its parent component has re-render triggers (like event handlers in `FunComponent`s, see the below section on classes.), the count will increase and updated. However if you want to work with dynamic data in your functions, you can use `Adaptive` values, `Stores`, or `Observables` which you can read further in [Interactive Nodes]. A brief example with adaptive values would be: @@ -101,7 +99,7 @@ type Counter() = button { onclick (fun _ -> let newCount = counter + 1 - Logger.LogDebug("Old Value: {count} - New Value: {newCount}", counter, newCount) + this.Logger.LogDebug("Old Value: {count} - New Value: {newCount}", counter, newCount) counter <- newCount ) "Increment" @@ -129,3 +127,6 @@ The drawbacks are: In general, classes can be used to orchestrate dependency injection, to use plain blazor features seamlessly and in general to keep as close as blazor as possible. Functions most of the time can overcome the benefits of classes when you use `html.comp` functions (also called `html.inject`) which you can read more about in [Hooks] and [Dependency Injection](<./Advanced-features/Dependency-injection-(DI)>) + +[Interactive Nodes]: ./Interactive-Nodes +[Hooks]: ./Advanced-features/Hook From 3f72760efd55fa3c9999267c2f79ab77d35c71e2 Mon Sep 17 00:00:00 2001 From: "Angel D. Munoz" Date: Thu, 30 Nov 2023 21:24:03 -0600 Subject: [PATCH 11/13] mention childContent in the list --- Docs/11 DOM/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Docs/11 DOM/README.md b/Docs/11 DOM/README.md index 6d638d45..1e54a9ac 100644 --- a/Docs/11 DOM/README.md +++ b/Docs/11 DOM/README.md @@ -72,8 +72,15 @@ ul { $"Item: {item}" } - // or also if you have an existing list of nodes +} + +``` +Or also if you have an existing list of nodes you can use the `childContent` operation. + +```fsharp +ul { + childContent listOfNodes } ``` From 23d06e5a71856f928e1622659cc8dd30a9e52739 Mon Sep 17 00:00:00 2001 From: "Angel D. Munoz" Date: Thu, 30 Nov 2023 21:37:11 -0600 Subject: [PATCH 12/13] Add corrections from feedback --- Docs/14 Classes and Functions/README.md | 32 +++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/Docs/14 Classes and Functions/README.md b/Docs/14 Classes and Functions/README.md index 5e462f18..0dcc645c 100644 --- a/Docs/14 Classes and Functions/README.md +++ b/Docs/14 Classes and Functions/README.md @@ -109,8 +109,37 @@ type Counter() = In this case we didn't add any dynamic values like Adaptive data, Stores or Observables and our counter is re-rendered and works as expected, blazor applies a diffing algorithm to know that it only needs to re-render the `h1` tag. you can also however use dynamic data if required. +### Using adaptive data + +If you would like to avoid re-rendering your components at event handlers, you can opt out of it by using `FunBlazorComponent` this base class turns off re-renders for event triggers. + +```fsharp +type Demo() = + // Note this is not the same as *inherit FunComponent()* + inherit FunBlazorComponent() + + let data = cval 0 + + override _.Render() = fragment { + h1 { "Static text" } + adaptiview() { + let! data = data + // only this part will need to be used for computate for diff when data is changed + p { $"x = {data}" } + } + button { + onclick (fun _ -> data.Publish(fun value -> value + 1)) + "Increase" + } + } +``` + +This has the added benefit that updates are more localized and you don't accidentally re-render on event handlers, most of the time it should not be an issue, but if you require to have more control of it, then this is the ideal way to do it. + ### Benefits and drawbacks +In general classes are good to use within Fun.Blazor and are a safe choice if your project needs them. + The benefits are: - Offer full decorator support `Parameter`, `Cascading Parameter`, `Route`, `Inject` @@ -120,11 +149,10 @@ The benefits are: The drawbacks are: -- Re-rendering requires diffing which can be expensive if you're not using dynamic data - They require manual bindings to play well with the Fun.Blazor DSL - Verbose -In general, classes can be used to orchestrate dependency injection, to use plain blazor features seamlessly and in general to keep as close as blazor as possible. +In general, classes can be used to orchestrate dependency injection, to use plain blazor features seamlessly, and to keep as close as blazor as possible. Functions most of the time can overcome the benefits of classes when you use `html.comp` functions (also called `html.inject`) which you can read more about in [Hooks] and [Dependency Injection](<./Advanced-features/Dependency-injection-(DI)>) From 4633abcca5af89ddc56493beb8415fc64fd90178 Mon Sep 17 00:00:00 2001 From: "Angel D. Munoz" Date: Thu, 30 Nov 2023 21:37:20 -0600 Subject: [PATCH 13/13] move links to the bottom --- Docs/40 Advanced/44 Working With Blazor/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Docs/40 Advanced/44 Working With Blazor/README.md b/Docs/40 Advanced/44 Working With Blazor/README.md index ecc8e5f0..76465adf 100644 --- a/Docs/40 Advanced/44 Working With Blazor/README.md +++ b/Docs/40 Advanced/44 Working With Blazor/README.md @@ -1,7 +1,3 @@ -[Fun.Blazor.Cli]: ./Tooling/Code-Generation -[Code Generation]: ./Tooling/Code-Generation -[Blazor Rendering Modes]: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-8.0 - # Working with Blazor Using Blazor features with almost "raw" blazor elements is possible in Fun.Blazor @@ -130,3 +126,7 @@ module Home = ``` This can be a tedious process however [Fun.Blazor.Cli] provides an automated tool to generate these, please visit the [Code Generation] section for more information on how to generate new bindings. + +[Fun.Blazor.Cli]: ./Tooling/Code-Generation +[Code Generation]: ./Tooling/Code-Generation +[Blazor Rendering Modes]: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-8.0