Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation updatates #54

Merged
merged 13 commits into from
Dec 1, 2023
269 changes: 139 additions & 130 deletions Docs/11 DOM/README.md
Original file line number Diff line number Diff line change
@@ -1,160 +1,169 @@
# DOM

The most important thing for **Fun.Blazor** is the DSL to build the DOM.
Even before V2, computation expression style DSL is already supported.
Fun.Blazor provides a friendly way to write HTML for web applications. It uses [F# Computation Expressions] to generate a simple yet performant DSL.

It is very easy to build and compose the DOM:
```fsharp
let hello name =
div {
id "my-id"
class' "my-class"
$"Hello, {name}"
}

hello "World!"
```

Calling that function would produce a markup like

```html
<div id="my-id" class="my-class">Hello, World!</div>
```

> 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.

### If/Else

```fsharp

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" }
}
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 commonAttr =
domAttr {
data 123
let element kind =
div {
$"The element is: "
region {
match kind with
| Fantastic -> "Fantastic"
| Average -> "Average"
| WellItsSomething -> "Wel... it is something"
}
}
```

### Lists

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
To render lists you can use `for item in items do`

```fsharp
ul {
h3 { "Some title." }
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"
}
}
```
}

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.
```

For CSS, you can do:
Or also if you have an existing list of nodes you can use the `childContent` operation.

```fsharp
type StyleBuilder with

[<CustomOperation("stack")>]
member inline _.stack([<InlineIfLambda>] comb: CombineKeyValue) =
comb
&&& css {
height "100%"
displayFlex
flexDirectionColumn
alignItemsStretch
overflowHidden
}

[<CustomOperation("strench")>]
member inline _.strench([<InlineIfLambda>] comb: CombineKeyValue) =
comb
&&& css {
flex 1
height "100%"
width "100%"
positionRelative
overflowXHidden
overflowYAuto
}


let demo =
div {
style { stack; backgroundColor "blue" }
div { "Header" }
div {
style { strench; backgroundColor "green" }
}
}
ul {
childContent listOfNodes
}
```

For DOM element/component, you can do:
> 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.

Without any bindings you can:
## 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 provide the builder with a string tuple.

```fsharp
let demo =
html.blazor (ComponentAttrBuilder<MudPaper>()
.Add((fun x -> x.Elevation), 10)
.Add((fun x -> x.Outlined), true)
)
section {
"my-attribute", "value"
}
```

With auto generated bindings:
### Shared attributes

If you'd like to share attributes between different elements you can use the `domAttr`

```fsharp
open Fun.Blazor
open Fun.Blazor.Operators
open MudBlazor

type MudTable'<'T> with

[<CustomOperation("HeaderAndRow")>]
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))))

[<CustomOperation("withDefaultSettings")>]
member inline _.withDefaultSettings([<InlineIfLambda>] render: AttrRenderFragment) =
render
==> MudTable'() {
Hover true
FixedHeader true
HorizontalScrollbar true
Breakpoint Breakpoint.None
asAttrRenderFragment // with this feature we can have a better coding experience
}

let demo =
MudTable'() {
Height "100%"
Items items
HeaderAndRow [
MudTh'() { "Name" },
fun item -> MudTd'() { item.Name }

MudTh'() { "Age" },
fun item -> MudTd'() { item.Age }
]
withDefaultSettings
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.
handlers can be async or sync depending on your usage but they're often defined as `EventArgs -> unit` or `EventArgs -> Task<unit>`

```fsharp
button {
AngelMunoz marked this conversation as resolved.
Show resolved Hide resolved
onclick(fun e -> printfn "clicked")
"Click Me"
}

button {
AngelMunoz marked this conversation as resolved.
Show resolved Hide resolved
onclick(fun e -> task {
do! Async.Sleep 1000
printfn "clicked"
})
"Click Me Task"
}
```

For inputs remember that events provide values as strings, so you have to unbox them

```fsharp
input {
placeholder "Write Something"
oninput(fun e ->
unbox<string> e.Value |> printfn "New Value: '%s'"
)
}

input {
type' "number"
placeholder "Change Number"
oninput(fun e ->
unbox<string> 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.

[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
Loading
Loading