├── .config
└── dotnet-tools.json
├── .gitignore
├── .paket
└── Paket.Restore.targets
├── LICENSE.md
├── README.md
├── WebSharper.Forms.Tests
├── Forms.fs
├── Main.fs
├── RenderUtils.fs
├── RenderWithTemplate.fs
├── RenderWithoutTemplate.fs
├── Web.config
├── WebSharper.Forms.Tests.fsproj
├── index.html
└── paket.references
├── WebSharper.Forms.sln
├── WebSharper.Forms
├── Forms.fs
├── Forms.fsi
├── WebSharper.Forms.fsproj
└── paket.references
├── build.cmd
├── build.fsx
├── build.sh
├── docs
└── Introduction.md
├── global.json
├── ivy.xml
├── nuget
└── WebSharper.Forms.paket.template
├── paket.dependencies
├── paket.lock
└── tools
└── WebSharper.snk
/.config/dotnet-tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "isRoot": true,
4 | "tools": {
5 | "paket": {
6 | "version": "8.0.3",
7 | "commands": [
8 | "paket"
9 | ]
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.suo
2 | *.user
3 | .vs/
4 | build/
5 | packages/
6 | tools/packages/
7 | build/
8 | *.Tests/Content/
9 | **/bin/
10 | **/obj/
11 |
12 | bin/
13 | obj/
14 | /.fake/
15 | /paket-files/
16 | /.vscode/
17 | websharper.log
18 |
--------------------------------------------------------------------------------
/.paket/Paket.Restore.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
8 |
9 | $(MSBuildVersion)
10 | 15.0.0
11 | false
12 | true
13 |
14 | true
15 | $(MSBuildThisFileDirectory)
16 | $(MSBuildThisFileDirectory)..\
17 | $(PaketRootPath)paket-files\paket.restore.cached
18 | $(PaketRootPath)paket.lock
19 | classic
20 | proj
21 | assembly
22 | native
23 | /Library/Frameworks/Mono.framework/Commands/mono
24 | mono
25 |
26 |
27 | $(PaketRootPath)paket.bootstrapper.exe
28 | $(PaketToolsPath)paket.bootstrapper.exe
29 | $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\
30 |
31 | "$(PaketBootStrapperExePath)"
32 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)"
33 |
34 |
35 |
36 |
37 | true
38 | true
39 |
40 |
41 | True
42 |
43 |
44 | False
45 |
46 | $(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/'))
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | $(PaketRootPath)paket
56 | $(PaketToolsPath)paket
57 |
58 |
59 |
60 |
61 |
62 | $(PaketRootPath)paket.exe
63 | $(PaketToolsPath)paket.exe
64 |
65 |
66 |
67 |
68 |
69 | <_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json"))
70 | <_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"'))
71 | <_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | <_PaketCommand>dotnet paket
83 |
84 |
85 |
86 |
87 |
88 | $(PaketToolsPath)paket
89 | $(PaketBootStrapperExeDir)paket
90 |
91 |
92 | paket
93 |
94 |
95 |
96 |
97 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)"))
98 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)"
99 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)"
100 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)"
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | true
122 | $(NoWarn);NU1603;NU1604;NU1605;NU1608
123 | false
124 | true
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)'))
134 |
135 |
136 |
137 |
138 |
139 |
141 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[0].Replace(`"`, ``).Replace(` `, ``))
142 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[1].Replace(`"`, ``).Replace(` `, ``))
143 |
144 |
145 |
146 |
147 | %(PaketRestoreCachedKeyValue.Value)
148 | %(PaketRestoreCachedKeyValue.Value)
149 |
150 |
151 |
152 |
153 | true
154 | false
155 | true
156 |
157 |
158 |
162 |
163 | true
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 | $(PaketIntermediateOutputPath)\$(MSBuildProjectFile).paket.references.cached
183 |
184 | $(MSBuildProjectFullPath).paket.references
185 |
186 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references
187 |
188 | $(MSBuildProjectDirectory)\paket.references
189 |
190 | false
191 | true
192 | true
193 | references-file-or-cache-not-found
194 |
195 |
196 |
197 |
198 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)'))
199 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)'))
200 | references-file
201 | false
202 |
203 |
204 |
205 |
206 | false
207 |
208 |
209 |
210 |
211 | true
212 | target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths)
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | false
224 | true
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length)
236 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0])
237 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1])
238 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4])
239 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5])
240 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[6])
241 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[7])
242 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[8])
243 |
244 |
245 | %(PaketReferencesFileLinesInfo.PackageVersion)
246 | All
247 | runtime
248 | $(ExcludeAssets);contentFiles
249 | $(ExcludeAssets);build;buildMultitargeting;buildTransitive
250 | %(PaketReferencesFileLinesInfo.Aliases)
251 | true
252 | true
253 |
254 |
255 |
256 |
257 |
258 | $(PaketIntermediateOutputPath)/$(MSBuildProjectFile).paket.clitools
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0])
268 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1])
269 |
270 |
271 | %(PaketCliToolFileLinesInfo.PackageVersion)
272 |
273 |
274 |
275 |
279 |
280 |
281 |
282 |
283 |
284 | false
285 |
286 |
287 |
288 |
289 |
290 | <_NuspecFilesNewLocation Include="$(PaketIntermediateOutputPath)\$(Configuration)\*.nuspec"/>
291 |
292 |
293 |
294 |
295 |
296 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile)
297 | true
298 | false
299 | true
300 | false
301 | true
302 | false
303 | true
304 | false
305 | true
306 | false
307 | true
308 | $(PaketIntermediateOutputPath)\$(Configuration)
309 | $(PaketIntermediateOutputPath)
310 |
311 |
312 |
313 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.$(PackageVersion.Split(`+`)[0]).nuspec"/>
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
373 |
374 |
423 |
424 |
469 |
470 |
514 |
515 |
558 |
559 |
560 |
561 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # Apache License
2 |
3 | Version 2.0, January 2004
4 |
5 | http://www.apache.org/licenses/
6 |
7 | ## TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
8 |
9 | 1. Definitions.
10 |
11 | "License" shall mean the terms and conditions for use, reproduction,
12 | and distribution as defined by Sections 1 through 9 of this document.
13 |
14 | "Licensor" shall mean the copyright owner or entity authorized by
15 | the copyright owner that is granting the License.
16 |
17 | "Legal Entity" shall mean the union of the acting entity and all
18 | other entities that control, are controlled by, or are under common
19 | control with that entity. For the purposes of this definition,
20 | "control" means (i) the power, direct or indirect, to cause the
21 | direction or management of such entity, whether by contract or
22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
23 | outstanding shares, or (iii) beneficial ownership of such entity.
24 |
25 | "You" (or "Your") shall mean an individual or Legal Entity
26 | exercising permissions granted by this License.
27 |
28 | "Source" form shall mean the preferred form for making modifications,
29 | including but not limited to software source code, documentation
30 | source, and configuration files.
31 |
32 | "Object" form shall mean any form resulting from mechanical
33 | transformation or translation of a Source form, including but
34 | not limited to compiled object code, generated documentation,
35 | and conversions to other media types.
36 |
37 | "Work" shall mean the work of authorship, whether in Source or
38 | Object form, made available under the License, as indicated by a
39 | copyright notice that is included in or attached to the work
40 | (an example is provided in the Appendix below).
41 |
42 | "Derivative Works" shall mean any work, whether in Source or Object
43 | form, that is based on (or derived from) the Work and for which the
44 | editorial revisions, annotations, elaborations, or other modifications
45 | represent, as a whole, an original work of authorship. For the purposes
46 | of this License, Derivative Works shall not include works that remain
47 | separable from, or merely link (or bind by name) to the interfaces of,
48 | the Work and Derivative Works thereof.
49 |
50 | "Contribution" shall mean any work of authorship, including
51 | the original version of the Work and any modifications or additions
52 | to that Work or Derivative Works thereof, that is intentionally
53 | submitted to Licensor for inclusion in the Work by the copyright owner
54 | or by an individual or Legal Entity authorized to submit on behalf of
55 | the copyright owner. For the purposes of this definition, "submitted"
56 | means any form of electronic, verbal, or written communication sent
57 | to the Licensor or its representatives, including but not limited to
58 | communication on electronic mailing lists, source code control systems,
59 | and issue tracking systems that are managed by, or on behalf of, the
60 | Licensor for the purpose of discussing and improving the Work, but
61 | excluding communication that is conspicuously marked or otherwise
62 | designated in writing by the copyright owner as "Not a Contribution."
63 |
64 | "Contributor" shall mean Licensor and any individual or Legal Entity
65 | on behalf of whom a Contribution has been received by Licensor and
66 | subsequently incorporated within the Work.
67 |
68 | 2. Grant of Copyright License. Subject to the terms and conditions of
69 | this License, each Contributor hereby grants to You a perpetual,
70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
71 | copyright license to reproduce, prepare Derivative Works of,
72 | publicly display, publicly perform, sublicense, and distribute the
73 | Work and such Derivative Works in Source or Object form.
74 |
75 | 3. Grant of Patent License. Subject to the terms and conditions of
76 | this License, each Contributor hereby grants to You a perpetual,
77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
78 | (except as stated in this section) patent license to make, have made,
79 | use, offer to sell, sell, import, and otherwise transfer the Work,
80 | where such license applies only to those patent claims licensable
81 | by such Contributor that are necessarily infringed by their
82 | Contribution(s) alone or by combination of their Contribution(s)
83 | with the Work to which such Contribution(s) was submitted. If You
84 | institute patent litigation against any entity (including a
85 | cross-claim or counterclaim in a lawsuit) alleging that the Work
86 | or a Contribution incorporated within the Work constitutes direct
87 | or contributory patent infringement, then any patent licenses
88 | granted to You under this License for that Work shall terminate
89 | as of the date such litigation is filed.
90 |
91 | 4. Redistribution. You may reproduce and distribute copies of the
92 | Work or Derivative Works thereof in any medium, with or without
93 | modifications, and in Source or Object form, provided that You
94 | meet the following conditions:
95 |
96 | (a) You must give any other recipients of the Work or
97 | Derivative Works a copy of this License; and
98 |
99 | (b) You must cause any modified files to carry prominent notices
100 | stating that You changed the files; and
101 |
102 | (c) You must retain, in the Source form of any Derivative Works
103 | that You distribute, all copyright, patent, trademark, and
104 | attribution notices from the Source form of the Work,
105 | excluding those notices that do not pertain to any part of
106 | the Derivative Works; and
107 |
108 | (d) If the Work includes a "NOTICE" text file as part of its
109 | distribution, then any Derivative Works that You distribute must
110 | include a readable copy of the attribution notices contained
111 | within such NOTICE file, excluding those notices that do not
112 | pertain to any part of the Derivative Works, in at least one
113 | of the following places: within a NOTICE text file distributed
114 | as part of the Derivative Works; within the Source form or
115 | documentation, if provided along with the Derivative Works; or,
116 | within a display generated by the Derivative Works, if and
117 | wherever such third-party notices normally appear. The contents
118 | of the NOTICE file are for informational purposes only and
119 | do not modify the License. You may add Your own attribution
120 | notices within Derivative Works that You distribute, alongside
121 | or as an addendum to the NOTICE text from the Work, provided
122 | that such additional attribution notices cannot be construed
123 | as modifying the License.
124 |
125 | You may add Your own copyright statement to Your modifications and
126 | may provide additional or different license terms and conditions
127 | for use, reproduction, or distribution of Your modifications, or
128 | for any such Derivative Works as a whole, provided Your use,
129 | reproduction, and distribution of the Work otherwise complies with
130 | the conditions stated in this License.
131 |
132 | 5. Submission of Contributions. Unless You explicitly state otherwise,
133 | any Contribution intentionally submitted for inclusion in the Work
134 | by You to the Licensor shall be under the terms and conditions of
135 | this License, without any additional terms or conditions.
136 | Notwithstanding the above, nothing herein shall supersede or modify
137 | the terms of any separate license agreement you may have executed
138 | with Licensor regarding such Contributions.
139 |
140 | 6. Trademarks. This License does not grant permission to use the trade
141 | names, trademarks, service marks, or product names of the Licensor,
142 | except as required for reasonable and customary use in describing the
143 | origin of the Work and reproducing the content of the NOTICE file.
144 |
145 | 7. Disclaimer of Warranty. Unless required by applicable law or
146 | agreed to in writing, Licensor provides the Work (and each
147 | Contributor provides its Contributions) on an "AS IS" BASIS,
148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
149 | implied, including, without limitation, any warranties or conditions
150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
151 | PARTICULAR PURPOSE. You are solely responsible for determining the
152 | appropriateness of using or redistributing the Work and assume any
153 | risks associated with Your exercise of permissions under this License.
154 |
155 | 8. Limitation of Liability. In no event and under no legal theory,
156 | whether in tort (including negligence), contract, or otherwise,
157 | unless required by applicable law (such as deliberate and grossly
158 | negligent acts) or agreed to in writing, shall any Contributor be
159 | liable to You for damages, including any direct, indirect, special,
160 | incidental, or consequential damages of any character arising as a
161 | result of this License or out of the use or inability to use the
162 | Work (including but not limited to damages for loss of goodwill,
163 | work stoppage, computer failure or malfunction, or any and all
164 | other commercial damages or losses), even if such Contributor
165 | has been advised of the possibility of such damages.
166 |
167 | 9. Accepting Warranty or Additional Liability. While redistributing
168 | the Work or Derivative Works thereof, You may choose to offer,
169 | and charge a fee for, acceptance of support, warranty, indemnity,
170 | or other liability obligations and/or rights consistent with this
171 | License. However, in accepting such obligations, You may act only
172 | on Your own behalf and on Your sole responsibility, not on behalf
173 | of any other Contributor, and only if You agree to indemnify,
174 | defend, and hold each Contributor harmless for any liability
175 | incurred by, or claims asserted against, such Contributor by reason
176 | of your accepting any such warranty or additional liability.
177 |
178 | ## END OF TERMS AND CONDITIONS
179 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WebSharper.Forms
2 |
3 | Forms are a functional, composable, and type-safe form abstraction for building reactive user interfaces in WebSharper,
4 | similar to Formlets but with fine control over the structure of the output.
5 |
6 | A sample Form:
7 |
8 | ```fsharp
9 | let LoginForm () =
10 | Form.Return (fun user pass -> user, pass)
11 | <*> (Form.Yield ""
12 | |> Validation.IsNotEmpty "Must enter a username")
13 | <*> (Form.Yield ""
14 | |> Validation.IsNotEmpty "Must enter a password")
15 | |> Form.WithSubmit
16 | |> Form.Run (fun (u, p) ->
17 | JS.Alert("Welcome, " + u + "!")
18 | )
19 | |> Form.Render (fun user pass submit ->
20 | div [] [
21 | div [] [label [] [text "Username: "]; Doc.Input [] user]
22 | div [] [label [] [text "Password: "]; Doc.PasswordBox [] pass]
23 | Doc.Button "Log in" [] submit.Trigger
24 | div [] [
25 | Doc.ShowErrors submit.View (fun errors ->
26 | errors
27 | |> Seq.map (fun m -> p [] [text m.Text])
28 | |> Doc.Concat)
29 | ]
30 | ]
31 | )
32 | ```
33 |
34 | * [Tutorial][intro] - check here first to learn about Forms
35 | * Demos
36 | * The [Pets example](http://try.websharper.com/snippet/Dark_Clark/0000Cy) from the tutorial on [Try WebSharper](https://try.websharper.com)
37 | * The main [test project](https://github.com/dotnet-websharper/forms/tree/master/WebSharper.Forms.Tests) in this repository - check here for inline HTML and *templated* forms
38 | * [License][license] (Apache v2)
39 | * GitHub - [sources][gh], [tracker][issues]
40 | * Community
41 | * [WebSharper on Gitter][gitter] - technical chat
42 | * [WebSharper Forums][wsforums] - Got a question?
43 | * [#websharper on freenode][chat]
44 | * [Need support?][contact] - IntelliFactory
45 |
46 | ## Wait, formlets and piglets? - I am confused
47 |
48 | `WebSharper.Forms` (this project, aka. **reactive** piglets or `WebSharper.UI.Piglets`) is a reactive implementation of the original [WebSharper.Piglets](https://github.com/dotnet-websharper/piglets) library, using [WebSharper.UI](https://github.com/dotnet-websharper/ui), [WebSharper](https://websharper.com)'s main reactive library.
49 |
50 | Piglets are a novel UI abstraction pioneered by WebSharper, and are first documented in this IntelliFactory research paper:
51 |
52 | > Loic Denuziere, Ernesto Rodriguez, Adam Granicz. **Piglets to the Rescue: Declarative User Interface Specification with Pluggable View Models**. In Symposium on Implementation and Application of Functional Languages (IFL), Nijmegen, The Netherlands, 2013. [ACM](https://dl.acm.org/citation.cfm?id=2620689), **[PDF](http://www.cs.ru.nl/P.Achten/IFL2013/symposium_proceedings_IFL2013/ifl2013_submission_29.pdf)**.
53 |
54 | Formlets have similarly been published in academia, among others in [this 2007 draft paper](https://www.cl.cam.ac.uk/~jdy22/papers/idioms-guide.pdf) by Ezra Cooper, Sam Lindley, Philip Wadler, and Jeremy Yallop at the University of Edinburgh.
55 |
56 | Formlets have first been ported to F# for WebSharper in 2009, enhanced for dependent flowlets and published in this IntelliFactory research paper:
57 |
58 | > Joel Bjornson, Anton Tayanovskyy, Adam Granicz. **Composing Reactive GUIs in F# Using WebSharper**. In Symposium on Implementation and Application of Functional Languages (IFL), Alphen aan den Rijn, The Netherlands, 2010. pp. 203-216. [Springer](https://link.springer.com/chapter/10.1007/978-3-642-24276-2_13)
59 |
60 | This early formlet library is available as [WebSharper.Formlets](https://github.com/dotnet-websharper/formlets), and a `WebSharper.UI`-based re-implementation is available as [WebSharper.UI.Formlets](https://github.com/dotnet-websharper/ui.formlets).
61 |
62 | Given that reactive forms/piglets are more flexible than formlets, we recommend that you use `WebSharper.Forms` (this project) in your applications.
63 |
64 |
65 | [chat]: http://webchat.freenode.net/?channels=#websharper
66 | [contact]: http://intellifactory.com/contact
67 | [wsforums]: https://forums.websharper.com/
68 | [fsharp]: http://fsharp.org
69 | [gh]: http://github.com/intellifactory/websharper.forms
70 | [gitter]: https://gitter.im/intellifactory/websharper
71 | [intro]: http://github.com/intellifactory/websharper.forms/blob/master/docs/Introduction.md
72 | [issues]: http://github.com/intellifactory/websharper.forms/issues
73 | [license]: http://github.com/intellifactory/websharper.forms/blob/master/LICENSE.md
74 | [nuget]: http://nuget.org
75 |
--------------------------------------------------------------------------------
/WebSharper.Forms.Tests/Forms.fs:
--------------------------------------------------------------------------------
1 | // $begin{copyright}
2 | //
3 | // This file is part of WebSharper
4 | //
5 | // Copyright (c) 2008-2018 IntelliFactory
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License"); you
8 | // may not use this file except in compliance with the License. You may
9 | // obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
16 | // implied. See the License for the specific language governing
17 | // permissions and limitations under the License.
18 | //
19 | // $end{copyright}
20 | namespace WebSharper.Forms.Tests
21 |
22 | open WebSharper
23 | open WebSharper.JavaScript
24 | open WebSharper.UI
25 | open WebSharper.Forms
26 |
27 | []
28 | module Forms =
29 |
30 | type Contact = Email of string | PhoneNumber of string
31 |
32 | let AddItemForm() =
33 | Form.Return (fun x y -> (x, y))
34 | <*> (Form.Yield "John Doe"
35 | |> Validation.IsNotEmpty "Please enter a name.")
36 | <*> Form.Do {
37 | let! isEmail = Form.Yield true
38 | if isEmail then
39 | return! Form.Yield "john@doe.com"
40 | |> Validation.IsMatch @"^.+@.+\..+$" "Please enter a valid email address."
41 | |> Form.Map Email
42 | else
43 | return! Form.Yield "01 234 5678"
44 | |> Validation.Is (fun s -> s.Length >= 6) "Please enter a valid phone number."
45 | |> Form.Map PhoneNumber
46 | }
47 | |> Form.WithSubmit
48 |
49 | let FullForm() =
50 | Form.ManyForm Seq.empty (AddItemForm()) Form.Yield
51 | |> Validation.Is (not << Seq.isEmpty) "Please enter at least one contact."
52 | |> Form.WithSubmit
53 |
--------------------------------------------------------------------------------
/WebSharper.Forms.Tests/Main.fs:
--------------------------------------------------------------------------------
1 | // $begin{copyright}
2 | //
3 | // This file is part of WebSharper
4 | //
5 | // Copyright (c) 2008-2018 IntelliFactory
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License"); you
8 | // may not use this file except in compliance with the License. You may
9 | // obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
16 | // implied. See the License for the specific language governing
17 | // permissions and limitations under the License.
18 | //
19 | // $end{copyright}
20 | namespace WebSharper.Forms.Tests
21 |
22 | open WebSharper
23 | open WebSharper.JavaScript
24 | open WebSharper.UI
25 | open WebSharper.UI.Html
26 | open WebSharper.UI.Client
27 | open WebSharper.Forms
28 |
29 | []
30 | module Main =
31 |
32 | []
33 | let Main() =
34 | Doc.Concat [
35 | h1 [] [text "Rendered with a template:"]
36 | RenderWithTemplate.Render()
37 | hr [] []
38 | h1 [] [text "Rendered with HTML combinators:"]
39 | RenderWithoutTemplate.Render()
40 | ]
41 | |> Doc.RunAppend JS.Document.Body
42 |
--------------------------------------------------------------------------------
/WebSharper.Forms.Tests/RenderUtils.fs:
--------------------------------------------------------------------------------
1 | // $begin{copyright}
2 | //
3 | // This file is part of WebSharper
4 | //
5 | // Copyright (c) 2008-2018 IntelliFactory
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License"); you
8 | // may not use this file except in compliance with the License. You may
9 | // obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
16 | // implied. See the License for the specific language governing
17 | // permissions and limitations under the License.
18 | //
19 | // $end{copyright}
20 | namespace WebSharper.Forms.Tests
21 |
22 | open WebSharper
23 | open WebSharper.JavaScript
24 | open WebSharper.UI
25 | open WebSharper.UI.Html
26 | open WebSharper.UI.Client
27 | open WebSharper.Forms
28 |
29 | []
30 | module RenderUtils =
31 |
32 | let ShowErrorMessage v =
33 | v |> Doc.BindView (function
34 | | Success _ -> Doc.Empty
35 | | Failure msgs ->
36 | Doc.Concat [
37 | for msg in msgs do
38 | yield text msg.Text
39 | yield br [] [] :> _
40 | ]
41 | )
42 |
--------------------------------------------------------------------------------
/WebSharper.Forms.Tests/RenderWithTemplate.fs:
--------------------------------------------------------------------------------
1 | // $begin{copyright}
2 | //
3 | // This file is part of WebSharper
4 | //
5 | // Copyright (c) 2008-2018 IntelliFactory
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License"); you
8 | // may not use this file except in compliance with the License. You may
9 | // obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
16 | // implied. See the License for the specific language governing
17 | // permissions and limitations under the License.
18 | //
19 | // $end{copyright}
20 | namespace WebSharper.Forms.Tests
21 |
22 | open WebSharper
23 | open WebSharper.JavaScript
24 | open WebSharper.UI
25 | open WebSharper.UI.Html
26 | open WebSharper.UI.Client
27 | open WebSharper.Forms
28 | open WebSharper.Forms.Tests.Forms
29 |
30 | #nowarn "58" // indentation
31 |
32 | []
33 | module RenderWithTemplate =
34 |
35 | type Template = Templating.Template<"index.html">
36 |
37 | let Render() =
38 | Forms.FullForm()
39 | |> Form.Render (fun items submit ->
40 | Template.Form()
41 | .Items(
42 | items.Render (fun ops rvContact ->
43 | Template.Item()
44 | .Name(rvContact.View |> View.Map fst)
45 | .Contact(rvContact.View |> View.Map (function
46 | | _, Email x -> "email: " + x
47 | | _, PhoneNumber x -> "phone: " + x)
48 | )
49 | .MoveUp(Attr.SubmitterValidate ops.MoveUp)
50 | .MoveDown(Attr.SubmitterValidate ops.MoveDown)
51 | .Delete(on.click (fun _ _ -> ops.Delete()))
52 | .Doc()
53 | )
54 | )
55 | .Adder(
56 | items.RenderAdder(fun rvName depContact submit ->
57 | Template.Adder()
58 | .Name(rvName)
59 | .NameErrors(ShowErrorMessage (submit.View.Through rvName))
60 | .ContactType(
61 | depContact.RenderPrimary (fun rvContactType ->
62 | Doc.Concat [
63 | label [] [Doc.Radio [] true rvContactType; text "Email"]
64 | label [] [Doc.Radio [] false rvContactType; text "Phone number"]
65 | ]
66 | )
67 | )
68 | .Contact(
69 | depContact.RenderDependent (fun rvContact ->
70 | Template.Contact()
71 | .ContactText(rvContact)
72 | .ContactErrors(ShowErrorMessage (submit.View.Through rvContact))
73 | .Doc()
74 | )
75 | )
76 | .Add(fun _ -> submit.Trigger())
77 | .Doc()
78 | )
79 | )
80 | .Submit(fun _ -> submit.Trigger())
81 | .SubmitResults([
82 | submit.View |> View.Map (function
83 | | Success xs ->
84 | Doc.Concat [
85 | for name, contact in xs ->
86 | let contact =
87 | match contact with
88 | | PhoneNumber n -> "phone: " + n
89 | | Email e -> "email: " + e
90 | Template.SubmitSuccess().Name(name).Contact(contact).Doc()
91 | ]
92 | | Failure msgs ->
93 | Doc.Concat [
94 | for msg in msgs ->
95 | Template.SubmitError().Message(msg.Text).Doc()
96 | ]
97 | )
98 | |> Doc.EmbedView
99 | ])
100 | .Doc()
101 | )
102 |
103 |
--------------------------------------------------------------------------------
/WebSharper.Forms.Tests/RenderWithoutTemplate.fs:
--------------------------------------------------------------------------------
1 | // $begin{copyright}
2 | //
3 | // This file is part of WebSharper
4 | //
5 | // Copyright (c) 2008-2018 IntelliFactory
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License"); you
8 | // may not use this file except in compliance with the License. You may
9 | // obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
16 | // implied. See the License for the specific language governing
17 | // permissions and limitations under the License.
18 | //
19 | // $end{copyright}
20 | namespace WebSharper.Forms.Tests
21 |
22 | open WebSharper
23 | open WebSharper.JavaScript
24 | open WebSharper.UI
25 | open WebSharper.UI.Html
26 | open WebSharper.UI.Client
27 | open WebSharper.Forms
28 | open WebSharper.Forms.Tests.Forms
29 |
30 | []
31 | module RenderWithoutTemplate =
32 |
33 | let Render() =
34 | Forms.FullForm()
35 | |> Form.Render (fun items submit ->
36 | div [] [
37 | h2 [] [text "Contacts:"]
38 | table [] [
39 | thead [] [
40 | tr [] [
41 | th [] [text "Name"]
42 | th [] [text "Contact"]
43 | ]
44 | ]
45 | tbody [] [
46 | items.Render (fun ops x ->
47 | tr [] [
48 | td [] [textView (x.View |> View.Map fst)]
49 | td [] [textView (x.View |> View.Map (function
50 | | _, Email x -> "email: " + x
51 | | _, PhoneNumber x -> "phone: " + x
52 | ))]
53 | td [] [Doc.ButtonValidate "Move up" [] ops.MoveUp]
54 | td [] [Doc.ButtonValidate "Move down" [] ops.MoveDown]
55 | td [] [button [on.click (fun _ _ -> ops.Delete())] [text "Delete"]]
56 | ]
57 | )
58 | ]
59 | ]
60 | div [attr.style "border: solid 1px #888; padding: 10px; margin: 20px"] [
61 | h3 [] [text "Add a new item"]
62 | items.RenderAdder (fun rvName depContact submit ->
63 | div [] [
64 | p [] [
65 | Doc.Input [] rvName
66 | ShowErrorMessage (submit.View.Through rvName)
67 | ]
68 | p [] [
69 | depContact.RenderPrimary (fun rvContactType ->
70 | div [] [
71 | label [] [Doc.Radio [] true rvContactType; text "Email"]
72 | label [] [Doc.Radio [] false rvContactType; text "Phone number"]
73 | ]
74 | )
75 | depContact.RenderDependent (fun rvContact ->
76 | Doc.Concat [
77 | Doc.Input [] rvContact
78 | ShowErrorMessage (submit.View.Through rvContact)
79 | ]
80 | )
81 | ]
82 | p [] [Doc.Button "Add" [] submit.Trigger]
83 | ]
84 | )
85 | ]
86 | Doc.Button "Submit" [] submit.Trigger
87 | submit.View |> View.Map (function
88 | | Success contacts ->
89 | div [] (
90 | contacts |> Seq.map (fun (x, contact) ->
91 | let contact =
92 | match contact with
93 | | Email e -> " (email: " + e + ")"
94 | | PhoneNumber n -> " (phone: " + n + ")"
95 | p [] [text ("You registered a contact: " + string x + contact)] :> Doc
96 | )
97 | )
98 | | Failure msgs -> div [attr.style "color:red"] [for msg in msgs -> p [] [text msg.Text] :> _]
99 | )
100 | |> Doc.EmbedView
101 | ]
102 | )
103 |
--------------------------------------------------------------------------------
/WebSharper.Forms.Tests/Web.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | True
13 |
14 |
15 |
16 |
17 | True
18 |
19 |
20 |
21 |
22 | True
23 |
24 |
25 |
26 |
27 | True
28 |
29 |
30 |
31 |
32 | True
33 |
34 |
35 |
36 |
37 | True
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/WebSharper.Forms.Tests/WebSharper.Forms.Tests.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net50
5 | Bundle
6 | true
7 | true
8 | true
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/WebSharper.Forms.Tests/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WebSharper.Forms.Tests
5 |
6 |
7 |
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
Contacts:
20 |
21 |
22 |
23 |
Name
24 |
Contact
25 |
26 |
27 |
28 |
29 |
${Name}
30 |
${Contact}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
Add a new contact
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | You registered a contact: ${Name} (${Contact})
62 |
63 |
64 | ${Message}
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/WebSharper.Forms.Tests/paket.references:
--------------------------------------------------------------------------------
1 | group testing
2 | WebSharper
3 | WebSharper.FSharp
4 | WebSharper.UI
5 | WebSharper.AspNetCore
6 | FSharp.Core
7 |
--------------------------------------------------------------------------------
/WebSharper.Forms.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 2013
4 | VisualStudioVersion = 12.0.31101.0
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7CA7EED6-C0D3-4C7B-A84F-5B07976AE62F}"
7 | ProjectSection(SolutionItems) = preProject
8 | build.fsx = build.fsx
9 | ivy.xml = ivy.xml
10 | LICENSE.md = LICENSE.md
11 | README.md = README.md
12 | EndProjectSection
13 | EndProject
14 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WebSharper.Forms", "WebSharper.Forms\WebSharper.Forms.fsproj", "{AB53BB7F-FBE4-4F4D-A817-7A7F20F2B06F}"
15 | EndProject
16 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WebSharper.Forms.Tests", "WebSharper.Forms.Tests\WebSharper.Forms.Tests.fsproj", "{87297620-C340-470F-A347-21DFA030C502}"
17 | ProjectSection(ProjectDependencies) = postProject
18 | {AB53BB7F-FBE4-4F4D-A817-7A7F20F2B06F} = {AB53BB7F-FBE4-4F4D-A817-7A7F20F2B06F}
19 | EndProjectSection
20 | EndProject
21 | Global
22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
23 | Debug|Any CPU = Debug|Any CPU
24 | Release|Any CPU = Release|Any CPU
25 | EndGlobalSection
26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
27 | {AB53BB7F-FBE4-4F4D-A817-7A7F20F2B06F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28 | {AB53BB7F-FBE4-4F4D-A817-7A7F20F2B06F}.Debug|Any CPU.Build.0 = Debug|Any CPU
29 | {AB53BB7F-FBE4-4F4D-A817-7A7F20F2B06F}.Release|Any CPU.ActiveCfg = Release|Any CPU
30 | {AB53BB7F-FBE4-4F4D-A817-7A7F20F2B06F}.Release|Any CPU.Build.0 = Release|Any CPU
31 | {87297620-C340-470F-A347-21DFA030C502}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32 | {87297620-C340-470F-A347-21DFA030C502}.Debug|Any CPU.Build.0 = Debug|Any CPU
33 | {87297620-C340-470F-A347-21DFA030C502}.Release|Any CPU.ActiveCfg = Release|Any CPU
34 | {87297620-C340-470F-A347-21DFA030C502}.Release|Any CPU.Build.0 = Release|Any CPU
35 | EndGlobalSection
36 | GlobalSection(SolutionProperties) = preSolution
37 | HideSolutionNode = FALSE
38 | EndGlobalSection
39 | EndGlobal
40 |
--------------------------------------------------------------------------------
/WebSharper.Forms/Forms.fs:
--------------------------------------------------------------------------------
1 | // $begin{copyright}
2 | //
3 | // This file is part of WebSharper
4 | //
5 | // Copyright (c) 2008-2018 IntelliFactory
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License"); you
8 | // may not use this file except in compliance with the License. You may
9 | // obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
16 | // implied. See the License for the specific language governing
17 | // permissions and limitations under the License.
18 | //
19 | // $end{copyright}
20 | namespace WebSharper.Forms
21 |
22 | open System.Runtime.CompilerServices
23 | open WebSharper
24 | open WebSharper.JavaScript
25 | open WebSharper.UI
26 | open WebSharper.UI.Client
27 | open WebSharper.UI.Notation
28 |
29 | []
30 | type ErrorMessage (id: string, message: string) =
31 |
32 | []
33 | member this.Id = id
34 |
35 | []
36 | member this.Text = message
37 |
38 | []
39 | module Fresh =
40 |
41 | let lastId = ref 0
42 |
43 | let Id() =
44 | incr lastId
45 | "Form" + string !lastId
46 |
47 | []
48 | type Result<'T> =
49 | | Success of 'T
50 | | Failure of list
51 |
52 | []
53 | type Result =
54 |
55 | static member IsSuccess (r: Result<'T>) =
56 | match r with
57 | | Success _ -> true
58 | | Failure _ -> false
59 |
60 | static member IsFailure (r: Result<'T>) =
61 | match r with
62 | | Success _ -> false
63 | | Failure _ -> true
64 |
65 | static member Map (f: 'T -> 'U) (r: Result<'T>) =
66 | match r with
67 | | Success x -> Success (f x)
68 | | Failure m -> Failure m
69 |
70 | static member Apply (rf: Result<'T -> 'U>) (rx: Result<'T>) =
71 | match rf with
72 | | Failure mf ->
73 | match rx with
74 | | Failure mx -> Failure (mf @ mx)
75 | | Success _ -> Failure mf
76 | | Success f ->
77 | match rx with
78 | | Failure mx -> Failure mx
79 | | Success x -> Success (f x)
80 |
81 | static member ApJoin (rf: Result<'T -> 'U>) (rx: Result>) =
82 | match rf with
83 | | Failure mf ->
84 | match rx with
85 | | Failure mx -> Failure (mf @ mx)
86 | | Success _ -> Failure mf
87 | | Success f ->
88 | match rx with
89 | | Failure mx
90 | | Success (Failure mx) -> Failure mx
91 | | Success (Success x) -> Success (f x)
92 |
93 | static member Bind (f: 'T -> Result<'U>) (r: Result<'T>) : Result<'U> =
94 | match r with
95 | | Failure m -> Failure m
96 | | Success x -> f x
97 |
98 | static member Append (app: 'T -> 'T -> 'T) (r1: Result<'T>) (r2: Result<'T>) : Result<'T> =
99 | match r1 with
100 | | Failure m1 ->
101 | match r2 with
102 | | Failure m2 -> Failure (m1 @ m2)
103 | | Success _ -> r1
104 | | Success x1 ->
105 | match r2 with
106 | | Failure _ -> r2
107 | | Success x2 -> Success (app x1 x2)
108 |
109 | static member FailWith (errorMessage, ?id) =
110 | let id = match id with Some id -> id | None -> Fresh.Id()
111 | Failure [ErrorMessage(id, errorMessage)]
112 |
113 | []
114 | type Form<'T, 'R> =
115 | {
116 | id : string
117 | view : View>
118 | render : 'R
119 | }
120 |
121 | []
122 | member this.Id = this.id
123 | []
124 | member this.View = this.view
125 | []
126 | member this.Render = this.render
127 |
128 | type ErrorMessage with
129 |
130 | []
131 | static member Create (id: string, text) =
132 | new ErrorMessage(id, text)
133 |
134 | []
135 | static member Create (p: Form<_, _>, text) =
136 | new ErrorMessage(p.id, text)
137 |
138 | []
139 | module Utils =
140 |
141 | let memoize f =
142 | let d = System.Collections.Generic.Dictionary()
143 | fun x ->
144 | if d.ContainsKey x then
145 | d.[x]
146 | else
147 | let y = f x
148 | d.[x] <- y
149 | y
150 |
151 |
152 | []
153 | module Form =
154 |
155 | []
156 | type Dependent<'TResult, 'U, 'W>
157 | (
158 | renderPrimary: 'U -> Doc,
159 | pOut: View Doc>>>
160 | ) =
161 |
162 | let out =
163 | pOut.Bind (function
164 | | Success p -> p.view
165 | | Failure m -> View.Const (Failure m))
166 |
167 | []
168 | member this.View : View> = out
169 |
170 | []
171 | member this.RenderPrimary (f: 'U) : Doc =
172 | renderPrimary f
173 |
174 | member this.RenderDependent (f: 'W) : Doc =
175 | pOut |> Doc.BindView (function
176 | | Success p -> p.render f
177 | | Failure _ -> Doc.Empty)
178 |
179 | module Dependent =
180 | let Make primary dependent =
181 | let dependent = memoize (fun x ->
182 | let p = dependent x
183 | { view = p.view; id = p.id; render = fun x -> p.render x :> Doc })
184 | let pOut = primary.view.Map (Result.Map dependent)
185 | Dependent((fun x -> primary.render x :> Doc), pOut)
186 |
187 |
188 | []
189 | module Many =
190 |
191 | type ItemOperations(delete: unit -> unit, moveUp: Submitter>, moveDown: Submitter>) =
192 | []
193 | member this.Delete() = delete()
194 | []
195 | member this.MoveUp = moveUp
196 | []
197 | member this.MoveDown = moveDown
198 |
199 | type System.Collections.Generic.List<'T> with
200 | member this.Swap(i, j) =
201 | let tmp = this.[i]
202 | this.[i] <- this.[j]
203 | this.[j] <- tmp
204 |
205 | module Fresh =
206 |
207 | let Int =
208 | let x = ref 0
209 | fun () ->
210 | incr x
211 | !x
212 |
213 | type Collection<'T, 'V, 'W, 'Y, 'Z when 'W :> Doc and 'Z :> Doc> (p : 'T -> Form<'T, 'V -> 'W>, inits: seq<'T>, adder : Form<'T, 'Y -> 'Z>) =
214 | let arr = ResizeArray()
215 | let var = Var.Create arr
216 | let mk (x: 'T) =
217 | let ident = Fresh.Int()
218 | let getThisIndexIn = Seq.findIndex (fun (_, _, j) -> ident = j)
219 | let vIndex = var.View |> View.Map getThisIndexIn
220 | let delete() =
221 | let k = getThisIndexIn arr
222 | arr.RemoveAt k
223 | Var.Update var id
224 | let sMoveUp =
225 | let inp = vIndex |> View.Map (fun i ->
226 | if i = 0 then Failure [] else Success true
227 | )
228 | Submitter.Create inp (if arr.Count = 0 then Failure [] else Success false)
229 | let vMoveUp =
230 | sMoveUp.View |> View.Map (function
231 | | Success true ->
232 | let i = getThisIndexIn arr
233 | arr.Swap(i, i - 1)
234 | Var.Update var id
235 | | _ -> ()
236 | )
237 | let sMoveDown =
238 | let inp = vIndex |> View.Map (fun i ->
239 | if i = arr.Count - 1 then Failure [] else Success true
240 | )
241 | Submitter.Create inp (Failure [])
242 | let vMoveDown =
243 | sMoveDown.View |> View.Map (function
244 | | Success true ->
245 | let i = getThisIndexIn arr
246 | arr.Swap(i, i + 1)
247 | Var.Update var id
248 | | _ -> ()
249 | )
250 | let p = p x
251 | let v = View.Map2 (fun x () -> x) p.view (View.Map2 (fun () () -> ()) vMoveUp vMoveDown)
252 | let p = { p with view = v }
253 | p, ItemOperations(delete, sMoveUp, sMoveDown), ident
254 | do Seq.iter (mk >> arr.Add) inits
255 |
256 | let fst3 (x, _, _) = x
257 | let changesView =
258 | var.View
259 | |> View.Bind (fun arr ->
260 | arr.ToArray()
261 | |> Array.MapTreeReduce
262 | (fun x ->
263 | (fst3 x).view |> View.Map (fun _ -> Seq.singleton x))
264 | (View.Const Seq.empty)
265 | (View.Map2 Seq.append)
266 | )
267 |
268 | let add x =
269 | arr.Add(mk x)
270 | Var.Update var id
271 |
272 | let adderView x =
273 | match x with
274 | | Failure _ -> ()
275 | | Success x -> add x
276 | Doc.Empty
277 |
278 | let out =
279 | var.View
280 | |> View.Bind (fun s ->
281 | s.ToArray()
282 | |> Array.MapTreeReduce
283 | (fun (p, _, _) -> p.view |> View.Map (Result.Map Seq.singleton))
284 | (View.Const (Success Seq.empty))
285 | (View.Map2 (Result.Append Seq.append))
286 | )
287 |
288 | []
289 | member this.View = out
290 |
291 | member this.Render (f: ItemOperations -> 'V) : Doc =
292 | changesView
293 | |> Doc.BindSeqCachedBy (fun (_, _, ident) -> ident) (fun (p, ops, _) ->
294 | p.render (f ops) :> Doc
295 | )
296 |
297 | []
298 | member this.Add (x: 'T) =
299 | add x
300 |
301 | member this.RenderAdder f =
302 | adder.render f
303 | |> Doc.Append (adder.view |> View.Map adderView |> Doc.EmbedView)
304 |
305 | []
306 | type CollectionWithDefault<'T, 'V, 'W when 'W :> Doc> (p, inits, pInit, ``default``) =
307 | inherit Collection<'T, 'V, 'W, 'V, 'W> (p, inits, pInit)
308 |
309 | member this.Add() = this.Add ``default``
310 |
311 | []
312 | let (>>^) v f = fun g -> g (v f)
313 |
314 | let Create view (renderBuilder: _ -> _) =
315 | {
316 | id = Fresh.Id()
317 | view = view
318 | render = renderBuilder
319 | }
320 |
321 | let Render renderFunction p =
322 | p.render renderFunction
323 | |> Doc.Append (
324 | p.view
325 | |> View.Map (fun _ -> Doc.Empty)
326 | |> Doc.EmbedView
327 | )
328 |
329 | []
330 | let RenderMany (c: Many.Collection<_,_,_,_,_>) f =
331 | c.Render f
332 |
333 | []
334 | let RenderManyAdder (c: Many.Collection<_,_,_,_,_>) f =
335 | c.RenderAdder f
336 |
337 | []
338 | let RenderPrimary (d: Dependent<_,_,_>) f =
339 | d.RenderPrimary f
340 |
341 | []
342 | let RenderDependent (d: Dependent<_,_,_>) f =
343 | d.RenderDependent f
344 |
345 | []
346 | let GetView (p: Form<_, _ -> _>) =
347 | p.view
348 |
349 | let Return value =
350 | {
351 | id = Fresh.Id()
352 | view = View.Const (Success value)
353 | render = id
354 | }
355 |
356 | let ReturnFailure () =
357 | {
358 | id = Fresh.Id()
359 | view = View.Const (Failure [])
360 | render = id
361 | }
362 |
363 | let YieldVar (var: Var<_>) =
364 | {
365 | id = var.Id
366 | view = var.View |> View.Map Success
367 | render = fun r -> r var
368 | }
369 |
370 | let Yield init =
371 | YieldVar (Var.Create init)
372 |
373 | let YieldFailure () =
374 | let var = Var.Create JS.Undefined<_> :> Var<_>
375 | let view = var.View
376 | {
377 | id = var.Id
378 | view = View.SnapshotOn (Failure []) view (view |> View.Map Success)
379 | render = fun r -> r var
380 | }
381 |
382 | let YieldOption init noneValue =
383 | let var = Var.Create (defaultArg init noneValue) :> Var<_>
384 | {
385 | id = var.Id
386 | view = var.View |> View.Map (fun x ->
387 | Success (if x = noneValue then None else Some x))
388 | render = fun r -> r var
389 | }
390 |
391 | let Apply pf px =
392 | {
393 | id = Fresh.Id()
394 | view = View.Map2 Result.Apply pf.view px.view
395 | render = pf.render >> px.render
396 | }
397 |
398 | let ApJoin pf px =
399 | {
400 | id = Fresh.Id()
401 | view = View.Map2 Result.ApJoin pf.view px.view
402 | render = pf.render >> px.render
403 | }
404 |
405 | let WithSubmit p =
406 | let submitter = Submitter.Create p.view (Failure [])
407 | {
408 | id = Fresh.Id()
409 | view = submitter.View
410 | render = fun r -> p.render r submitter
411 | }
412 |
413 | let TransmitView p =
414 | {
415 | id = p.id
416 | view = p.view
417 | render = fun x -> p.render x p.view
418 | }
419 |
420 | let TransmitViewMapResult f p =
421 | {
422 | id = p.id
423 | view = p.view
424 | render = fun x -> p.render x (View.Map f p.view)
425 | }
426 |
427 | let TransmitViewMap f p =
428 | TransmitViewMapResult (Result.Map f) p
429 |
430 | let MapResult f p : Form<_, _ -> _> =
431 | {
432 | id = p.id
433 | view = View.Map f p.view
434 | render = p.render
435 | }
436 |
437 | let MapToResult f p =
438 | MapResult (Result.Bind f) p
439 |
440 | let Map f p =
441 | MapResult (Result.Map f) p
442 |
443 | let MapAsyncResult f p : Form<_, _ -> _> =
444 | {
445 | id = p.id
446 | view = View.MapAsync f p.view
447 | render = p.render
448 | }
449 |
450 | let MapToAsyncResult f p =
451 | let f x =
452 | match x with
453 | | Success x -> async { return! f x }
454 | | Failure m -> async { return Failure m }
455 | MapAsyncResult f p
456 |
457 | let MapAsync f p =
458 | let f x =
459 | match x with
460 | | Success x -> async { let! y = f x in return Success y }
461 | | Failure m -> async { return Failure m }
462 | MapAsyncResult f p
463 |
464 | let MapRenderArgs f p =
465 | {
466 | id = p.id
467 | view = p.view
468 | render = fun g -> g (p.render f)
469 | }
470 |
471 | let FlushErrors p =
472 | MapResult (function Failure _ -> Failure [] | x -> x) p
473 |
474 | let Run f p =
475 | Map (fun x -> f x; x) p
476 |
477 | let RunResult f p =
478 | MapResult (fun x -> f x; x) p
479 |
480 | []
481 | let ManyForm init addForm itemForm =
482 | let m = Many.Collection(itemForm, init, addForm)
483 | {
484 | id = Fresh.Id()
485 | view = m.View
486 | render = fun f -> f m
487 | }
488 |
489 | []
490 | let Many init addValue itemForm =
491 | let pInit = itemForm addValue
492 | let m = Many.CollectionWithDefault(itemForm, init, pInit, addValue)
493 | {
494 | id = Fresh.Id()
495 | view = m.View
496 | render = fun f -> f m
497 | }
498 |
499 | let Dependent primary dependent =
500 | let d = Dependent.Make primary dependent
501 | {
502 | id = Fresh.Id()
503 | view = d.View
504 | render = fun f -> f d
505 | }
506 |
507 | type Builder =
508 | | Do
509 |
510 | []
511 | member this.Bind(input, output) = Dependent input output
512 |
513 | []
514 | member this.Return x = Return x
515 |
516 | []
517 | member this.ReturnFrom (p: Form<_, _ -> _>) = p
518 |
519 | []
520 | member this.Yield init = Yield init
521 |
522 | []
523 | member this.YieldFrom (p: Form<_, _ -> _>) = p
524 |
525 | []
526 | member this.Zero() = ReturnFailure()
527 |
528 | []
529 | module Validation =
530 |
531 | let Is pred msg p =
532 | p |> Form.MapResult (fun res ->
533 | match res with
534 | | Success x -> if pred x then res else Failure [ErrorMessage(p.id, msg)]
535 | | Failure _ -> res
536 | )
537 |
538 | let IsNotEmpty msg p =
539 | Is (fun x -> x <> "") msg p
540 |
541 | let IsMatch (regexp: string) msg p =
542 | Is (RegExp(regexp).Test) msg p
543 |
544 | let MapValidCheckedInput msg p =
545 | p |> Form.MapResult (fun res ->
546 | match res with
547 | | Success (CheckedInput.Valid (x, _)) -> Success x
548 | | Success _ -> Failure [ErrorMessage.Create(p, msg)]
549 | | Failure msgs -> Failure msgs
550 | )
551 |
552 | []
553 | []
554 | module Pervasives =
555 |
556 | let (<*>) pf px =
557 | Form.Apply pf px
558 |
559 | let (<*?>) pf px =
560 | Form.ApJoin pf px
561 |
562 | []
563 | module Attr =
564 |
565 | open WebSharper.UI.Html
566 | open WebSharper.UI.Client
567 |
568 | let SubmitterValidate (submitter: Submitter<_>) =
569 | Attr.Append
570 | (on.click (fun _ _ -> submitter.Trigger()))
571 | (attr.disabledDynPred (View.Const "disabled")
572 | (submitter.Input |> View.Map Result.IsFailure))
573 |
574 | []
575 | module Doc =
576 |
577 | open WebSharper.UI.Html
578 | open WebSharper.UI.Client
579 |
580 | let ButtonValidate caption attrs (submitter: Submitter<_>) =
581 | Elt.button (Seq.append [|Attr.SubmitterValidate submitter|] attrs) [text caption]
582 |
583 | let ShowErrors (v: View>) (f: list -> Doc) =
584 | v.Doc(function
585 | | Success _ -> Doc.Empty
586 | | Failure msgs -> f msgs
587 | )
588 |
589 | let ShowSuccess (v: View>) (f: 'T -> Doc) =
590 | v.Doc(function
591 | | Success x -> f x
592 | | Failure msgs -> Doc.Empty
593 | )
594 |
595 | []
596 | type View =
597 |
598 | []
599 | static member Through (input: View>, v: Var<'U>) : View> =
600 | input |> View.Map (fun x ->
601 | match x with
602 | | Success _ -> x
603 | | Failure msgs -> Failure (msgs |> List.filter (fun m -> m.Id = v.Id))
604 | )
605 |
606 | []
607 | static member Through (input: View>, p: Form<'U, 'R>) : View> =
608 | input |> View.Map (fun x ->
609 | match x with
610 | | Success _ -> x
611 | | Failure msgs -> Failure (msgs |> List.filter (fun m -> m.Id = p.id))
612 | )
613 |
614 | []
615 | static member ShowErrors (this: View>, f: list -> Doc) : Doc =
616 | Doc.ShowErrors this f
617 |
618 | []
619 | static member ShowSuccess (this: View>, f: 'T -> Doc) : Doc =
620 | Doc.ShowSuccess this f
621 |
--------------------------------------------------------------------------------
/WebSharper.Forms/Forms.fsi:
--------------------------------------------------------------------------------
1 | // $begin{copyright}
2 | //
3 | // This file is part of WebSharper
4 | //
5 | // Copyright (c) 2008-2018 IntelliFactory
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License"); you
8 | // may not use this file except in compliance with the License. You may
9 | // obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
16 | // implied. See the License for the specific language governing
17 | // permissions and limitations under the License.
18 | //
19 | // $end{copyright}
20 | namespace WebSharper.Forms
21 |
22 | open System.Runtime.CompilerServices
23 | open WebSharper
24 | open WebSharper.UI
25 | open WebSharper.UI.Client
26 |
27 | []
28 | type ErrorMessage =
29 | member Id: string
30 | member Text: string
31 | static member Create : id: string * text: string -> ErrorMessage
32 | static member Create : Form<'T, 'R> * text: string -> ErrorMessage
33 |
34 | and Result<'T> =
35 | | Success of 'T
36 | | Failure of list
37 |
38 | and [] Form<'T, 'R> =
39 | member Id : string
40 | member View : View>
41 | member Render : 'R
42 |
43 | []
44 | type Result =
45 |
46 | /// Check whether a result is successful.
47 | static member IsSuccess
48 | : Result<'T>
49 | -> bool
50 |
51 | /// Check whether a result is failing.
52 | static member IsFailure
53 | : Result<'T>
54 | -> bool
55 |
56 | /// Pass a result through a function if it is successful.
57 | static member Map
58 | : f: ('T -> 'U)
59 | -> r: Result<'T>
60 | -> Result<'U>
61 |
62 | /// Apply a function result to a value result if both are successful.
63 | static member Apply
64 | : rf: Result<'T -> 'U>
65 | -> rx: Result<'T>
66 | -> Result<'U>
67 |
68 | /// Pass a result through a function if it is successful.
69 | static member Bind
70 | : f: ('T -> Result<'U>)
71 | -> r: Result<'T>
72 | -> Result<'U>
73 |
74 | /// Create a failing result with a single error message.
75 | static member FailWith
76 | : errorMessage: string
77 | * ?id: string
78 | -> Result<'T>
79 |
80 |
81 | /// Form constructors and combinators.
82 | module Form =
83 |
84 | /// Operations related to Forms of collections.
85 | module Many =
86 |
87 | /// Operations applicable to an item in a Form of collections.
88 | []
89 | type ItemOperations =
90 |
91 | /// Delete the current item from the collection.
92 | member Delete : unit -> unit
93 |
94 | /// Move the current item up one step in the collection.
95 | member MoveUp : Submitter>
96 |
97 | /// Move the current item down one step in the collection.
98 | member MoveDown : Submitter>
99 |
100 | /// Operations applicable to a Form of collections.
101 | []
102 | type Collection<'T, 'V, 'W, 'Y, 'Z when 'W :> Doc and 'Z :> Doc> =
103 |
104 | /// A view on the resulting collection.
105 | member View : View>>
106 |
107 | /// Render the item collection inside this Form
108 | /// with the provided rendering function.
109 | member Render : (ItemOperations -> 'V) -> Doc
110 |
111 | /// Stream where new items for the collection are written.
112 | member Add : 'T -> unit
113 |
114 | /// Render the Form that inserts new items into the collection.
115 | member RenderAdder : 'Y -> Doc
116 |
117 | /// Operations applicable to a Form of collections
118 | /// with a provided default new value to insert.
119 | []
120 | type CollectionWithDefault<'T, 'V, 'W when 'W :> Doc> =
121 | inherit Collection<'T,'V,'W,'V,'W>
122 |
123 | /// Add an item to the collection set to the default value.
124 | []
125 | member Add : unit -> unit
126 |
127 | /// Operations applicable to a dependent Form.
128 | []
129 | type Dependent<'TResult, 'U, 'W> =
130 |
131 | /// A view on the result of the dependent Form.
132 | member View : View>
133 |
134 | /// Render the primary part of a dependent Form.
135 | member RenderPrimary : 'U -> Doc
136 |
137 | /// Render the dependent part of a dependent Form.
138 | member RenderDependent : 'W -> Doc
139 |
140 | /// Create a Form from a view and a render builder.
141 | val Create
142 | : view: View>
143 | -> renderBuilder: ('R -> 'D)
144 | -> Form<'T, 'R -> 'D>
145 |
146 | /// Render a Form with a render function.
147 | val Render
148 | : renderFunction: 'R
149 | -> Form<'T, 'R -> #Doc>
150 | -> Doc
151 |
152 | /// Render the items of a Collection with the provided rendering function.
153 | val RenderMany
154 | : Many.Collection<'T, 'V, 'W, 'Y, 'Z>
155 | -> (Many.ItemOperations -> 'V)
156 | -> Doc
157 | when 'W :> Doc and 'Z :> Doc
158 |
159 | /// Render the Form that inserts new items into a Collection.
160 | val RenderManyAdder
161 | : Many.Collection<'T, 'V, 'W, 'Y, 'Z>
162 | -> 'Y
163 | -> Doc
164 | when 'W :> Doc and 'Z :> Doc
165 |
166 | /// Render the primary part of a dependent Form.
167 | val RenderPrimary
168 | : Dependent<'TResult, 'U, 'W>
169 | -> 'U
170 | -> Doc
171 |
172 | /// Render the dependent part of a dependent Form.
173 | val RenderDependent
174 | : Dependent<'TResult, 'U, 'W>
175 | -> 'W
176 | -> Doc
177 |
178 | /// Get the view of a Form.
179 | val GetView
180 | : Form<'T, 'R -> 'D>
181 | -> View>
182 |
183 | /// Create a Form that always returns the same successful value.
184 | val Return
185 | : value: 'T
186 | -> Form<'T, 'D -> 'D>
187 |
188 | /// Create a Form that always fails.
189 | val ReturnFailure
190 | : unit
191 | -> Form<'T, 'D -> 'D>
192 |
193 | /// Create a Form that returns a reactive value,
194 | /// initialized to a successful value `init`.
195 | val Yield
196 | : init: 'T
197 | -> Form<'T, (Var<'T> -> 'D) -> 'D>
198 |
199 | /// Create a Form that returns a reactive value.
200 | val YieldVar
201 | : Var<'T>
202 | -> Form<'T, (Var<'T> -> 'D) -> 'D>
203 |
204 | /// Create a Form that returns a reactive value, initialized to failure.
205 | val YieldFailure
206 | : unit
207 | -> Form<'T, (Var<'T> -> 'D) -> 'D>
208 |
209 | /// Create a Form that returns a reactive optional value,
210 | /// initialized to a successful value `init`.
211 | ///
212 | /// When the associated Var is `noneValue`, the result value is `None`;
213 | /// when it is any other value `x`, the result value is `Some x`.
214 | val YieldOption
215 | : init: option<'T>
216 | -> noneValue: 'T
217 | -> Form