|
1 | 1 | # DOM
|
2 | 2 |
|
3 |
| -The most important thing for **Fun.Blazor** is the DSL to build the DOM. |
4 |
| -Even before V2, computation expression style DSL is already supported. |
| 3 | +Fun.Blazor provides a friendly way to write HTML for web applications. It uses [F# Computation Expressions] to generate a simple yet performant DSL. |
5 | 4 |
|
6 |
| -It is very easy to build and compose the DOM: |
| 5 | +```fsharp |
| 6 | +let hello name = |
| 7 | + div { |
| 8 | + id "my-id" |
| 9 | + class' "my-class" |
| 10 | + $"Hello, {name}" |
| 11 | + } |
| 12 | +
|
| 13 | +hello "World!" |
| 14 | +``` |
| 15 | + |
| 16 | +Calling that function would produce a markup like |
| 17 | + |
| 18 | +```html |
| 19 | +<div id="my-id" class="my-class">Hello, World!</div> |
| 20 | +``` |
| 21 | + |
| 22 | +> 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` |
| 23 | +
|
| 24 | +> Note: Attributes must be placed before any other elements, like strings or other nodes |
| 25 | +
|
| 26 | +## Control Flow |
| 27 | + |
| 28 | +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. |
| 29 | + |
| 30 | +### If/Else |
7 | 31 |
|
8 | 32 | ```fsharp
|
9 | 33 |
|
10 |
| -let fragment1 = div { "F=ma" } |
11 |
| -
|
12 |
| -let composed = |
13 |
| - div { |
14 |
| - style { height 500 } |
15 |
| - p { "This is the way!" } |
16 |
| - // because below stuff will display something according some conditions, |
17 |
| - // 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. |
18 |
| - region { |
19 |
| - if conditionIsTrue then |
20 |
| - fragment1 |
21 |
| - p { "E = MCC" } |
22 |
| - } |
| 34 | +let element isVisible = |
| 35 | + div { |
| 36 | + $"The element is: " |
| 37 | + region { |
| 38 | + if isVisible then |
| 39 | + "Visible" |
| 40 | + else |
| 41 | + "Not Visible" |
23 | 42 | }
|
| 43 | + } |
24 | 44 | ```
|
25 | 45 |
|
26 |
| -Also, you can build a shared attributes/style fragment to compose: |
| 46 | +### Match |
27 | 47 |
|
28 | 48 | ```fsharp
|
29 |
| -let commonStyle = |
30 |
| - css { |
31 |
| - cursorPointer |
32 |
| - color "green" |
33 |
| - } |
34 | 49 |
|
35 |
| -let commonAttr = |
36 |
| - domAttr { |
37 |
| - data 123 |
| 50 | +let element kind = |
| 51 | + div { |
| 52 | + $"The element is: " |
| 53 | + region { |
| 54 | + match kind with |
| 55 | + | Fantastic -> "Fantastic" |
| 56 | + | Average -> "Average" |
| 57 | + | WellItsSomething -> "Wel... it is something" |
38 | 58 | }
|
| 59 | + } |
| 60 | +``` |
| 61 | + |
| 62 | +### Lists |
39 | 63 |
|
40 |
| -let sharedButtonAttrs = |
41 |
| - button { |
42 |
| - style { |
43 |
| - commonStyle |
44 |
| - // css priority/override is controlled by browser. |
45 |
| - // For "color", "red" will be used. |
46 |
| - color "red" |
47 |
| - } |
48 |
| - data 456 |
49 |
| - // attribute priority is controlled by blazor core. |
50 |
| - // Currently, only the first added attribute will be used when you are trying to add the same attribute. |
51 |
| - // That is why I put commonAttr lower than "data 456", |
52 |
| - // so the 456 will be used even in commonAttr "data" is 123. |
53 |
| - commonAttr |
54 |
| - asAttrRenderFragment // Here is the thing to make the magic happen |
| 64 | +To render lists you can use `for item in items do` |
| 65 | + |
| 66 | +```fsharp |
| 67 | +ul { |
| 68 | + h3 { "Some title." } |
| 69 | + for item in 0..10 do |
| 70 | + li { |
| 71 | + key item |
| 72 | + $"Item: {item}" |
55 | 73 | }
|
56 | 74 |
|
57 |
| -let demo = |
58 |
| - div { |
59 |
| - p { "Below we will have a cool button" } |
60 |
| - button { |
61 |
| - onclick ignore |
62 |
| - sharedButtonAttrs |
63 |
| - "Cool" |
64 |
| - } |
65 |
| - } |
66 |
| -``` |
| 75 | +} |
67 | 76 |
|
68 |
| -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. |
| 77 | +``` |
69 | 78 |
|
70 |
| -For CSS, you can do: |
| 79 | +Or also if you have an existing list of nodes you can use the `childContent` operation. |
71 | 80 |
|
72 | 81 | ```fsharp
|
73 |
| -type StyleBuilder with |
74 |
| -
|
75 |
| - [<CustomOperation("stack")>] |
76 |
| - member inline _.stack([<InlineIfLambda>] comb: CombineKeyValue) = |
77 |
| - comb |
78 |
| - &&& css { |
79 |
| - height "100%" |
80 |
| - displayFlex |
81 |
| - flexDirectionColumn |
82 |
| - alignItemsStretch |
83 |
| - overflowHidden |
84 |
| - } |
85 |
| -
|
86 |
| - [<CustomOperation("strench")>] |
87 |
| - member inline _.strench([<InlineIfLambda>] comb: CombineKeyValue) = |
88 |
| - comb |
89 |
| - &&& css { |
90 |
| - flex 1 |
91 |
| - height "100%" |
92 |
| - width "100%" |
93 |
| - positionRelative |
94 |
| - overflowXHidden |
95 |
| - overflowYAuto |
96 |
| - } |
97 |
| -
|
98 |
| -
|
99 |
| -let demo = |
100 |
| - div { |
101 |
| - style { stack; backgroundColor "blue" } |
102 |
| - div { "Header" } |
103 |
| - div { |
104 |
| - style { strench; backgroundColor "green" } |
105 |
| - } |
106 |
| - } |
| 82 | +ul { |
| 83 | + childContent listOfNodes |
| 84 | +} |
107 | 85 | ```
|
108 | 86 |
|
109 |
| -For DOM element/component, you can do: |
| 87 | +> 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. |
110 | 88 |
|
111 |
| -Without any bindings you can: |
| 89 | +## Attributes |
| 90 | + |
| 91 | +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. |
112 | 92 |
|
113 | 93 | ```fsharp
|
114 |
| -let demo = |
115 |
| - html.blazor (ComponentAttrBuilder<MudPaper>() |
116 |
| - .Add((fun x -> x.Elevation), 10) |
117 |
| - .Add((fun x -> x.Outlined), true) |
118 |
| - ) |
| 94 | +section { |
| 95 | + "my-attribute", "value" |
| 96 | +} |
119 | 97 | ```
|
120 | 98 |
|
121 |
| -With auto generated bindings: |
| 99 | +### Shared attributes |
| 100 | + |
| 101 | +If you'd like to share attributes between different elements you can use the `domAttr` |
122 | 102 |
|
123 | 103 | ```fsharp
|
124 |
| -open Fun.Blazor |
125 |
| -open Fun.Blazor.Operators |
126 |
| -open MudBlazor |
127 |
| -
|
128 |
| -type MudTable'<'T> with |
129 |
| -
|
130 |
| - [<CustomOperation("HeaderAndRow")>] |
131 |
| - member this.HeaderAndRow(render: AttrRenderFragment, mappers: (NodeRenderFragment * ('T -> NodeRenderFragment)) seq) = |
132 |
| - let headers = mappers |> Seq.map fst |
133 |
| - let render = this.HeaderContent(render, html.fragment headers) |
134 |
| - this.RowTemplate(render, (fun row -> html.inject (row, (fun () -> mappers |> Seq.map (snd >> fun fn -> fn row) |> html.fragment)))) |
135 |
| -
|
136 |
| - [<CustomOperation("withDefaultSettings")>] |
137 |
| - member inline _.withDefaultSettings([<InlineIfLambda>] render: AttrRenderFragment) = |
138 |
| - render |
139 |
| - ==> MudTable'() { |
140 |
| - Hover true |
141 |
| - FixedHeader true |
142 |
| - HorizontalScrollbar true |
143 |
| - Breakpoint Breakpoint.None |
144 |
| - asAttrRenderFragment // with this feature we can have a better coding experience |
145 |
| - } |
146 |
| -
|
147 |
| -let demo = |
148 |
| - MudTable'() { |
149 |
| - Height "100%" |
150 |
| - Items items |
151 |
| - HeaderAndRow [ |
152 |
| - MudTh'() { "Name" }, |
153 |
| - fun item -> MudTd'() { item.Name } |
154 |
| -
|
155 |
| - MudTh'() { "Age" }, |
156 |
| - fun item -> MudTd'() { item.Age } |
157 |
| - ] |
158 |
| - withDefaultSettings |
| 104 | +module SharedAttrs = |
| 105 | + let classAndData = |
| 106 | + domAttr { |
| 107 | + class' "has-data" |
| 108 | + data("my-data", "123") |
159 | 109 | }
|
160 |
| -``` |
| 110 | +
|
| 111 | +let someNode() = |
| 112 | + div { |
| 113 | + SharedAttrs.classAndData |
| 114 | + "Some Node" |
| 115 | + } |
| 116 | +
|
| 117 | +let otherNode() = |
| 118 | + div { |
| 119 | + SharedAttrs.classAndData |
| 120 | + "Other Node" |
| 121 | + } |
| 122 | +
|
| 123 | +``` |
| 124 | + |
| 125 | +## Events |
| 126 | + |
| 127 | +Events conform to the standard HTML event names, so you will find them in any element as usual. |
| 128 | +handlers can be async or sync depending on your usage but they're often defined as `EventArgs -> unit` or `EventArgs -> Task<unit>` |
| 129 | + |
| 130 | +```fsharp |
| 131 | +button { |
| 132 | + onclick(fun e -> printfn "clicked") |
| 133 | + "Click Me" |
| 134 | +} |
| 135 | +
|
| 136 | +button { |
| 137 | + onclick(fun e -> task { |
| 138 | + do! Async.Sleep 1000 |
| 139 | + printfn "clicked" |
| 140 | + }) |
| 141 | + "Click Me Task" |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +For inputs remember that events provide values as strings, so you have to unbox them |
| 146 | + |
| 147 | +```fsharp |
| 148 | +input { |
| 149 | + placeholder "Write Something" |
| 150 | + oninput(fun e -> |
| 151 | + unbox<string> e.Value |> printfn "New Value: '%s'" |
| 152 | + ) |
| 153 | +} |
| 154 | +
|
| 155 | +input { |
| 156 | + type' "number" |
| 157 | + placeholder "Change Number" |
| 158 | + oninput(fun e -> |
| 159 | + unbox<string> e.Value |> int |> printfn "New Value: '%i'" |
| 160 | + ) |
| 161 | +} |
| 162 | +``` |
| 163 | + |
| 164 | +> 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. |
| 165 | +
|
| 166 | +[F# Computation Expressions]: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions |
| 167 | +[Adaptive Data]: https://github.com/fsprojects/FSharp.Data.Adaptive |
| 168 | +[Working With Blazor]: ./Advanced-features/Working-With-Blazor |
| 169 | +[Adaptive Forms]: ./Advanced-features/Adaptive/Form |
0 commit comments