├── .gitignore
├── LICENSE
├── README.md
├── articles
├── cookbook-chapter-01.md
├── cookbook-chapter-01.org
├── cookbook-chapter-02.md
├── cookbook-chapter-02.org
├── cookbook-chapter-03.md
├── cookbook-chapter-03.org
├── cookbook-chapter-04.md
├── cookbook-chapter-04.org
├── cookbook-chapter-05.md
├── cookbook-chapter-05.org
├── cookbook-chapter-06.md
├── cookbook-chapter-06.org
├── cookbook-introduction.md
├── cookbook-introduction.org
└── images
│ ├── cookbook-chapter-01-image-01.png
│ ├── cookbook-chapter-02-image-01.png
│ ├── cookbook-chapter-02-image-02.png
│ ├── cookbook-chapter-02-image-03.png
│ ├── cookbook-chapter-03-image-01.png
│ ├── cookbook-chapter-04-image-01.png
│ ├── cookbook-chapter-05-image-01.png
│ ├── cookbook-chapter-06-image-01.png
│ └── websharper-default-project.png
└── code
├── chapter-01
├── WebSharperTutorial.FrontEnd
│ ├── Main.fs
│ ├── Startup.fs
│ ├── WebSharperTutorial.FrontEnd.fsproj
│ ├── appsettings.json
│ ├── templates
│ │ └── Main.html
│ └── wsconfig.json
└── WebSharperTutorial.sln
├── chapter-02
├── WebSharperTutorial.FrontEnd
│ ├── Main.fs
│ ├── Resources.fs
│ ├── Routes.fs
│ ├── Startup.fs
│ ├── WebSharperTutorial.FrontEnd.fsproj
│ ├── appsettings.json
│ ├── templates
│ │ └── Main.html
│ ├── wsconfig.json
│ └── wwwroot
│ │ ├── app
│ │ ├── css
│ │ │ └── common.css
│ │ └── js
│ │ │ └── common.js
│ │ └── vendor
│ │ └── bootstrap
│ │ ├── css
│ │ ├── bootstrap-grid.css
│ │ ├── bootstrap-grid.css.map
│ │ ├── bootstrap-grid.min.css
│ │ ├── bootstrap-grid.min.css.map
│ │ ├── bootstrap-reboot.css
│ │ ├── bootstrap-reboot.css.map
│ │ ├── bootstrap-reboot.min.css
│ │ ├── bootstrap-reboot.min.css.map
│ │ ├── bootstrap.css
│ │ ├── bootstrap.css.map
│ │ ├── bootstrap.min.css
│ │ └── bootstrap.min.css.map
│ │ └── js
│ │ ├── bootstrap.bundle.js
│ │ ├── bootstrap.bundle.js.map
│ │ ├── bootstrap.bundle.min.js
│ │ ├── bootstrap.bundle.min.js.map
│ │ ├── bootstrap.js
│ │ ├── bootstrap.js.map
│ │ ├── bootstrap.min.js
│ │ └── bootstrap.min.js.map
└── WebSharperTutorial.sln
├── chapter-03
├── WebSharperTutorial.FrontEnd
│ ├── Auth.fs
│ ├── Main.fs
│ ├── Page.Login.fs
│ ├── Resources.fs
│ ├── Routes.fs
│ ├── Startup.fs
│ ├── WebSharperTutorial.FrontEnd.fsproj
│ ├── appsettings.json
│ ├── templates
│ │ ├── Main.html
│ │ └── Page.Login.html
│ ├── wsconfig.json
│ └── wwwroot
│ │ ├── app
│ │ ├── css
│ │ │ └── common.css
│ │ └── js
│ │ │ └── common.js
│ │ └── vendor
│ │ └── bootstrap
│ │ ├── css
│ │ ├── bootstrap-grid.css
│ │ ├── bootstrap-grid.css.map
│ │ ├── bootstrap-grid.min.css
│ │ ├── bootstrap-grid.min.css.map
│ │ ├── bootstrap-reboot.css
│ │ ├── bootstrap-reboot.css.map
│ │ ├── bootstrap-reboot.min.css
│ │ ├── bootstrap-reboot.min.css.map
│ │ ├── bootstrap.css
│ │ ├── bootstrap.css.map
│ │ ├── bootstrap.min.css
│ │ └── bootstrap.min.css.map
│ │ └── js
│ │ ├── bootstrap.bundle.js
│ │ ├── bootstrap.bundle.js.map
│ │ ├── bootstrap.bundle.min.js
│ │ ├── bootstrap.bundle.min.js.map
│ │ ├── bootstrap.js
│ │ ├── bootstrap.js.map
│ │ ├── bootstrap.min.js
│ │ └── bootstrap.min.js.map
└── WebSharperTutorial.sln
├── chapter-04
├── WebSharperTutorial.FrontEnd
│ ├── Auth.fs
│ ├── Component.NavigationBar.fs
│ ├── Main.fs
│ ├── Page.Home.fs
│ ├── Page.Login.fs
│ ├── Resources.fs
│ ├── Routes.fs
│ ├── Startup.fs
│ ├── WebSharperTutorial.FrontEnd.fsproj
│ ├── appsettings.json
│ ├── templates
│ │ ├── Main.html
│ │ └── Page.Login.html
│ ├── wsconfig.json
│ └── wwwroot
│ │ ├── app
│ │ ├── css
│ │ │ └── common.css
│ │ └── js
│ │ │ └── common.js
│ │ └── vendor
│ │ └── bootstrap
│ │ ├── css
│ │ ├── bootstrap-grid.css
│ │ ├── bootstrap-grid.css.map
│ │ ├── bootstrap-grid.min.css
│ │ ├── bootstrap-grid.min.css.map
│ │ ├── bootstrap-reboot.css
│ │ ├── bootstrap-reboot.css.map
│ │ ├── bootstrap-reboot.min.css
│ │ ├── bootstrap-reboot.min.css.map
│ │ ├── bootstrap.css
│ │ ├── bootstrap.css.map
│ │ ├── bootstrap.min.css
│ │ └── bootstrap.min.css.map
│ │ └── js
│ │ ├── bootstrap.bundle.js
│ │ ├── bootstrap.bundle.js.map
│ │ ├── bootstrap.bundle.min.js
│ │ ├── bootstrap.bundle.min.js.map
│ │ ├── bootstrap.js
│ │ ├── bootstrap.js.map
│ │ ├── bootstrap.min.js
│ │ └── bootstrap.min.js.map
└── WebSharperTutorial.sln
├── chapter-05
├── WebSharperTutorial.FrontEnd
│ ├── Auth.fs
│ ├── Component.NavigationBar.fs
│ ├── DTO.fs
│ ├── Main.fs
│ ├── Page.Home.fs
│ ├── Page.Listing.fs
│ ├── Page.Login.fs
│ ├── Resources.fs
│ ├── Routes.fs
│ ├── Server.fs
│ ├── Startup.fs
│ ├── WebSharperTutorial.FrontEnd.fsproj
│ ├── appsettings.json
│ ├── templates
│ │ ├── Main.html
│ │ ├── Page.Listing.html
│ │ └── Page.Login.html
│ ├── wsconfig.json
│ └── wwwroot
│ │ ├── app
│ │ ├── css
│ │ │ └── common.css
│ │ └── js
│ │ │ └── common.js
│ │ └── vendor
│ │ └── bootstrap
│ │ ├── css
│ │ ├── bootstrap-grid.css
│ │ ├── bootstrap-grid.css.map
│ │ ├── bootstrap-grid.min.css
│ │ ├── bootstrap-grid.min.css.map
│ │ ├── bootstrap-reboot.css
│ │ ├── bootstrap-reboot.css.map
│ │ ├── bootstrap-reboot.min.css
│ │ ├── bootstrap-reboot.min.css.map
│ │ ├── bootstrap.css
│ │ ├── bootstrap.css.map
│ │ ├── bootstrap.min.css
│ │ └── bootstrap.min.css.map
│ │ └── js
│ │ ├── bootstrap.bundle.js
│ │ ├── bootstrap.bundle.js.map
│ │ ├── bootstrap.bundle.min.js
│ │ ├── bootstrap.bundle.min.js.map
│ │ ├── bootstrap.js
│ │ ├── bootstrap.js.map
│ │ ├── bootstrap.min.js
│ │ └── bootstrap.min.js.map
└── WebSharperTutorial.sln
└── chapter-06
├── WebSharperTutorial.FrontEnd
├── Auth.fs
├── Component.NavigationBar.fs
├── DTO.fs
├── Main.fs
├── Page.Form.fs
├── Page.Home.fs
├── Page.Listing.fs
├── Page.Login.fs
├── Resources.fs
├── Routes.fs
├── Server.fs
├── Startup.fs
├── WebSharperTutorial.FrontEnd.fsproj
├── appsettings.json
├── templates
│ ├── Main.html
│ ├── Page.Form.html
│ ├── Page.Listing.html
│ └── Page.Login.html
├── wsconfig.json
└── wwwroot
│ ├── app
│ ├── css
│ │ └── common.css
│ └── js
│ │ └── common.js
│ └── vendor
│ └── bootstrap
│ ├── css
│ ├── bootstrap-grid.css
│ ├── bootstrap-grid.css.map
│ ├── bootstrap-grid.min.css
│ ├── bootstrap-grid.min.css.map
│ ├── bootstrap-reboot.css
│ ├── bootstrap-reboot.css.map
│ ├── bootstrap-reboot.min.css
│ ├── bootstrap-reboot.min.css.map
│ ├── bootstrap.css
│ ├── bootstrap.css.map
│ ├── bootstrap.min.css
│ └── bootstrap.min.css.map
│ └── js
│ ├── bootstrap.bundle.js
│ ├── bootstrap.bundle.js.map
│ ├── bootstrap.bundle.min.js
│ ├── bootstrap.bundle.min.js.map
│ ├── bootstrap.js
│ ├── bootstrap.js.map
│ ├── bootstrap.min.js
│ └── bootstrap.min.js.map
└── WebSharperTutorial.sln
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 | obj/
3 | **/Content/WebSharper/
4 | **/Scripts/WebSharper/
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Creative Commons Legal Code
2 |
3 | CC0 1.0 Universal
4 |
5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
12 | HEREUNDER.
13 |
14 | Statement of Purpose
15 |
16 | The laws of most jurisdictions throughout the world automatically confer
17 | exclusive Copyright and Related Rights (defined below) upon the creator
18 | and subsequent owner(s) (each and all, an "owner") of an original work of
19 | authorship and/or a database (each, a "Work").
20 |
21 | Certain owners wish to permanently relinquish those rights to a Work for
22 | the purpose of contributing to a commons of creative, cultural and
23 | scientific works ("Commons") that the public can reliably and without fear
24 | of later claims of infringement build upon, modify, incorporate in other
25 | works, reuse and redistribute as freely as possible in any form whatsoever
26 | and for any purposes, including without limitation commercial purposes.
27 | These owners may contribute to the Commons to promote the ideal of a free
28 | culture and the further production of creative, cultural and scientific
29 | works, or to gain reputation or greater distribution for their Work in
30 | part through the use and efforts of others.
31 |
32 | For these and/or other purposes and motivations, and without any
33 | expectation of additional consideration or compensation, the person
34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she
35 | is an owner of Copyright and Related Rights in the Work, voluntarily
36 | elects to apply CC0 to the Work and publicly distribute the Work under its
37 | terms, with knowledge of his or her Copyright and Related Rights in the
38 | Work and the meaning and intended legal effect of CC0 on those rights.
39 |
40 | 1. Copyright and Related Rights. A Work made available under CC0 may be
41 | protected by copyright and related or neighboring rights ("Copyright and
42 | Related Rights"). Copyright and Related Rights include, but are not
43 | limited to, the following:
44 |
45 | i. the right to reproduce, adapt, distribute, perform, display,
46 | communicate, and translate a Work;
47 | ii. moral rights retained by the original author(s) and/or performer(s);
48 | iii. publicity and privacy rights pertaining to a person's image or
49 | likeness depicted in a Work;
50 | iv. rights protecting against unfair competition in regards to a Work,
51 | subject to the limitations in paragraph 4(a), below;
52 | v. rights protecting the extraction, dissemination, use and reuse of data
53 | in a Work;
54 | vi. database rights (such as those arising under Directive 96/9/EC of the
55 | European Parliament and of the Council of 11 March 1996 on the legal
56 | protection of databases, and under any national implementation
57 | thereof, including any amended or successor version of such
58 | directive); and
59 | vii. other similar, equivalent or corresponding rights throughout the
60 | world based on applicable law or treaty, and any national
61 | implementations thereof.
62 |
63 | 2. Waiver. To the greatest extent permitted by, but not in contravention
64 | of, applicable law, Affirmer hereby overtly, fully, permanently,
65 | irrevocably and unconditionally waives, abandons, and surrenders all of
66 | Affirmer's Copyright and Related Rights and associated claims and causes
67 | of action, whether now known or unknown (including existing as well as
68 | future claims and causes of action), in the Work (i) in all territories
69 | worldwide, (ii) for the maximum duration provided by applicable law or
70 | treaty (including future time extensions), (iii) in any current or future
71 | medium and for any number of copies, and (iv) for any purpose whatsoever,
72 | including without limitation commercial, advertising or promotional
73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
74 | member of the public at large and to the detriment of Affirmer's heirs and
75 | successors, fully intending that such Waiver shall not be subject to
76 | revocation, rescission, cancellation, termination, or any other legal or
77 | equitable action to disrupt the quiet enjoyment of the Work by the public
78 | as contemplated by Affirmer's express Statement of Purpose.
79 |
80 | 3. Public License Fallback. Should any part of the Waiver for any reason
81 | be judged legally invalid or ineffective under applicable law, then the
82 | Waiver shall be preserved to the maximum extent permitted taking into
83 | account Affirmer's express Statement of Purpose. In addition, to the
84 | extent the Waiver is so judged Affirmer hereby grants to each affected
85 | person a royalty-free, non transferable, non sublicensable, non exclusive,
86 | irrevocable and unconditional license to exercise Affirmer's Copyright and
87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the
88 | maximum duration provided by applicable law or treaty (including future
89 | time extensions), (iii) in any current or future medium and for any number
90 | of copies, and (iv) for any purpose whatsoever, including without
91 | limitation commercial, advertising or promotional purposes (the
92 | "License"). The License shall be deemed effective as of the date CC0 was
93 | applied by Affirmer to the Work. Should any part of the License for any
94 | reason be judged legally invalid or ineffective under applicable law, such
95 | partial invalidity or ineffectiveness shall not invalidate the remainder
96 | of the License, and in such case Affirmer hereby affirms that he or she
97 | will not (i) exercise any of his or her remaining Copyright and Related
98 | Rights in the Work or (ii) assert any associated claims and causes of
99 | action with respect to the Work, in either case contrary to Affirmer's
100 | express Statement of Purpose.
101 |
102 | 4. Limitations and Disclaimers.
103 |
104 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
105 | surrendered, licensed or otherwise affected by this document.
106 | b. Affirmer offers the Work as-is and makes no representations or
107 | warranties of any kind concerning the Work, express, implied,
108 | statutory or otherwise, including without limitation warranties of
109 | title, merchantability, fitness for a particular purpose, non
110 | infringement, or the absence of latent or other defects, accuracy, or
111 | the present or absence of errors, whether or not discoverable, all to
112 | the greatest extent permissible under applicable law.
113 | c. Affirmer disclaims responsibility for clearing rights of other persons
114 | that may apply to the Work or any use thereof, including without
115 | limitation any person's Copyright and Related Rights in the Work.
116 | Further, Affirmer disclaims responsibility for obtaining any necessary
117 | consents, permissions or other rights required for any use of the
118 | Work.
119 | d. Affirmer understands and acknowledges that Creative Commons is not a
120 | party to this document and has no duty or obligation with respect to
121 | this CC0 or use of the Work.
122 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WebSharper Cookbook Tutorial
2 | A WebSharper tutorial for client-server project
3 |
4 | ## Articles:
5 | 1. [Introduction](articles/cookbook-introduction.md)
6 | 2. [Chapter 01 - Foundations](articles/cookbook-chapter-01.md)
7 | 3. [Chapter 02 - Routing and Resources](articles/cookbook-chapter-02.md)
8 | 4. [Chapter 03 - ASP.NET Authentication](articles/cookbook-chapter-03.md)
9 | 5. [Chapter 04 - Site Navigation and more about the routing system](articles/cookbook-chapter-04.md)
10 | 6. [Chapter 05 - The listing page](articles/cookbook-chapter-05.md)
11 | 7. [Chapter 06 - The form page](articles/cookbook-chapter-06.md)
12 |
13 | ## Source code
14 | 1. [Chapter 01 - Foundations](code/chapter-01)
15 | 2. [Chapter 02 - Routing and Resources](code/chapter-02)
16 | 3. [Chapter 03 - ASP.NET Authentication](code/chapter-03)
17 | 4. [Chapter 04 - Site Navigation and more about the routing system](code/chapter-04)
18 | 5. [Chapter 05 - The listing page](code/chapter-05)
19 | 6. [Chapter 06 - The form page](code/chapter-06)
20 |
--------------------------------------------------------------------------------
/articles/cookbook-chapter-01.org:
--------------------------------------------------------------------------------
1 | * Chapter 01 - Foundations
2 | ** Creating the WebSharper project
3 | The final application has only one .NET project, but you probably might want to
4 | split it as per your needs.
5 |
6 | First, let's install the WebSharper templates:
7 | #+BEGIN_SRC bash
8 | $ dotnet new -i WebSharper.Templates
9 | #+END_SRC
10 |
11 | After installing the [[http://www.websharper.com/downloads][WebSharper templates]], open a console (I'm using bash) and
12 | create the web client-server project as:
13 |
14 | #+BEGIN_SRC bash
15 | $ mkdir tutorial
16 | $ cd tutorial
17 | $ dotnet new websharper-web -lang f# -n WebSharperTutorial.FrontEnd
18 | #+END_SRC
19 |
20 | The last command creates a folder named WebSharperTutorial.FrontEnd containing a few
21 | source code and setup files for the basic project.
22 |
23 | Let's create a solution and add the project to it:
24 |
25 | #+BEGIN_SRC bash
26 | $ dotnet new sln -n WebSharperTutorial
27 | $ dotnet sln WebSharperTutorial.sln add WebSharperTutorial.FrontEnd/WebSharperTutorial.FrontEnd.fsproj
28 | #+END_SRC
29 |
30 | Now, let's build and test it:
31 | #+BEGIN_SRC bash
32 | $ dotnet build
33 | $ dotnet run --project WebSharperTutorial.FrontEnd/WebSharperTutorial.FrontEnd.fsproj
34 | #+END_SRC
35 |
36 | If you load the test page at http://localhost:5000/, you might see the following
37 | page at your default browser now:
38 |
39 | #+CAPTION: WebSharper Default Project
40 | #+NAME: fig:WST-PRINT0001
41 | [[./images/websharper-default-project.png]]
42 |
43 | ** Project requirements
44 | This tutorial contains the following UI structure:
45 | - home page
46 | - login page
47 | - access denied page
48 | - listing page (access restricted)
49 | - form page (access restricted)
50 |
51 | Both listing and form pages are only available after the user gets logged in.
52 |
53 | Below, the list of requirements for each page
54 |
55 | - home page
56 | - navbar with:
57 | - brand
58 | - link to login page
59 | - login page
60 | - navbar: same as home page
61 | - form
62 | - listing page
63 | - navbar with:
64 | - link to itself
65 | - logout
66 | - table for listing data
67 | - form page
68 | - navbar: same as listing page
69 | - form
70 |
71 | Additional requirements:
72 | - Bootstrap 4
73 | - each page might have it's own URL
74 | - long calls to the server might display a animated gif
75 |
76 | ** The basic HTML template
77 | First, let's create the basic HTML frame for the application. For this, we are
78 | going the make use of WebSharper's template engine.
79 |
80 | Let's create a new folder named "templates" at the WebSharperTutorial.FrontEnd folder:
81 | #+BEGIN_SRC bash
82 | $ cd WebSharperTutorial.FrontEnd
83 | $ mkdir templates
84 | $ mv Main.html templates/
85 | #+END_SRC
86 |
87 | Now, open the templates/Main.html file with your chosen editor and delete its
88 | content. We are going to create a new one from scratch.
89 |
90 | As we are going to use Bootstrap 4, let's copy and paste the recommended
91 | template from their website and paste it in templates/Main.html file
92 |
93 | #+BEGIN_SRC html
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | Hello, world!
105 |
106 |
107 | Hello, world!
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | #+END_SRC
117 |
118 | *** Template and special attributes
119 | This is a good time to introduce the WebSharper's template engine.
120 |
121 | WebSharper allows us to create a template from a HTML file through the F# Type
122 | Provider.
123 |
124 | The template, once loaded in your code, allows composition with other elements and
125 | also to change its content through the */ws-holes/* and */ws-replace/* attributes. The
126 | difference between them, is the latter will replace its container element, while
127 | the former will insert the new content into the container element.
128 |
129 | WebSharper also provides three special attributes: */scripts/*, */meta/* and */styles/*.
130 | These attributes are reserved ones used by the framework to inject embedded
131 | resources and the transpiled scripts into the template files.
132 |
133 | Let's add them to the */Main.html/* template, by replacing it by the following:
134 |
135 | #+BEGIN_SRC html
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 | ${Title}
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 | #+END_SRC
164 |
165 | Notice the *${Title}* placeholder at the ** HTML tag. This is used for
166 | readonly data. WebSharper also provides placeholders for reactive variables,
167 | which we are going to rely on, when building the listing and form pages.
168 |
169 | Also, there is a */div/* with the *ws-replace="Body"* attribute. This placeholder will
170 | be used by to render the pages' contents.
171 |
172 | ** Consuming the basic HTML template
173 | Now that we have the basic HTML frame create, the next step is to use it from the
174 | F# code.
175 |
176 | Let's create a new */Main.fs/* file to load and render this template. Also remove
177 | those created by the WebSharper template project.
178 |
179 | From the WebSharperTutorial.FrontEnd folder:
180 |
181 | #+BEGIN_SRC bash
182 | $ rm Remoting.fs
183 | $ rm Client.fs
184 | $ rm Site.fs
185 | $ touch Main.fs
186 | #+END_SRC
187 |
188 | #+BEGIN_QUOTE
189 | Note: the */touch/* command just create a new file, on Linux. If you are using
190 | Windows, just create a new file using your editor or IDE.
191 | #+END_QUOTE
192 |
193 | Edit the WebSharperTutorial.FrontEnd.fsproj file, remove the reference for the
194 | deleted files and add a reference to the new one. This is how mine looks like
195 | after this change:
196 |
197 | #+BEGIN_SRC xml
198 |
199 |
200 |
201 | netcoreapp3.1
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 | #+END_SRC
220 |
221 | Edit the */Main.fs/* file and add the following code:
222 |
223 | #+BEGIN_SRC fsharp
224 | namespace WebSharperTutorial.FrontEnd
225 |
226 | open WebSharper
227 | open WebSharper.Sitelets
228 | open WebSharper.UI
229 | open WebSharper.UI.Server
230 |
231 | type EndPoint =
232 | | [] Home
233 |
234 | module Site =
235 | open WebSharper.UI.Html
236 |
237 | type MainTemplate = Templating.Template<"templates/Main.html">
238 |
239 | let private MainTemplate ctx action (title: string) (body: Doc list) =
240 | Content.Page(
241 | MainTemplate()
242 | .Title(title)
243 | .Body(body)
244 | .Doc()
245 | )
246 |
247 | let HomePage ctx =
248 | MainTemplate ctx EndPoint.Home "Home" [
249 | h1 [] [text "It works!"]
250 | div [] [ text "Hi there!" ]
251 | ]
252 |
253 | []
254 | let Main =
255 | Application.MultiPage (fun ctx endpoint ->
256 | match endpoint with
257 | | EndPoint.Home -> HomePage ctx
258 | )
259 |
260 | #+END_SRC
261 |
262 | Build and run it again:
263 |
264 | #+BEGIN_SRC bash
265 | $ dotnet build
266 | $ donet run # if you are in the WebSharperTutorial.FrontEnd directory
267 | # if from the solution directory
268 | $ dotnet run --project WebSharperTutorial.FrontEnd/WebSharperTutorial.FrontEnd.fsproj
269 | #+END_SRC
270 |
271 | This is what you might see:
272 |
273 | #+CAPTION: The Empty Layout
274 | #+NAME: fig:WST-PRINT0002
275 | [[./images/cookbook-chapter-01-image-01.png]]
276 |
277 |
278 | |----------+----+------------------------------------|
279 | | [[./cookbook-introduction.org][previous]] | [[../README.md][up]] | [[./cookbook-chapter-02.org][Chapter 02 - Routing and Resources]] |
280 | |----------+----+------------------------------------|
281 |
282 |
--------------------------------------------------------------------------------
/articles/cookbook-chapter-02.md:
--------------------------------------------------------------------------------
1 | - [Chapter 02 - Routing and Resources](#sec-1)
2 | - [the routing system](#sec-1-1)
3 | - [embedding resources](#sec-1-2)
4 |
5 | # Chapter 02 - Routing and Resources
6 |
7 | ## the routing system
8 |
9 | At this point, we've created the basic structure for the project. In this section, we are going to add support for navigation among web pages and also see how to use embedded resources.
10 |
11 | Additionally, we are going to change the ***Main.fs*** file to handle the routes and render the corresponding page.
12 |
13 | 
14 |
15 | Let's get started.
16 |
17 | Create a new file named ***Routes.fs*** and add it to ***.fsproj*** file. This file must come before the ***Main.fs***.
18 |
19 | > Note: from now on, whenever you create a new file, make sure to add it into the ***.fsproj*** file, as I'm not going to mention it anymore.
20 |
21 | ```xml
22 |
23 | ...
24 |
25 |
26 | ...
27 | ```
28 |
29 | Edit the ***Routes.fs*** file and add to following content to it:
30 |
31 | ```fsharp
32 | namespace WebSharperTutorial.FrontEnd
33 |
34 | open System
35 | open WebSharper
36 | open WebSharper.Sitelets
37 | open WebSharper.UI
38 |
39 | module Routes =
40 |
41 | []
42 | type EndPoint =
43 | | [] Home
44 | | [] Login
45 | | [] AccessDenied
46 | | [] Listing
47 | | [] Form of int64
48 |
49 | ```
50 |
51 | As you can see, we've created 5 endpoints, one for each page in the application.
52 |
53 | Notice the ` [] ` attribute at the **EndPoint** type. This attribute is used by WebSharper compiler to transpile this type onto the JavaScript equivalent one.
54 |
55 | The ` [] ` attribute before each discriminated union option makes them available to WebSharper routing engine.
56 |
57 | This attribute provides a few options to setup the route for each endpoint, such as declaring the HTTP method (GET,POST) and the URL path, e.g. ` [] `
58 |
59 | But for this tutorial, we are going to use a more advantageous configuration, by customizing the `Router<'T>` mapping.
60 |
61 | The next code lists this customized router. Add it to the end of the ***Routes.fs*** file:
62 |
63 | ```fsharp
64 | (* Router is used by both client and server side *)
65 | []
66 | let SiteRouter : Router =
67 | let link endPoint =
68 | match endPoint with
69 | | Home -> [ ]
70 | | Login -> [ "login" ]
71 | | AccessDenied -> [ "access-denied" ]
72 | | Listing -> [ "private"; "listing" ]
73 | | Form code -> [ "private"; "form"; string code ]
74 |
75 | let route (path) =
76 | match path with
77 | | [ ] -> Some Home
78 | | [ "login" ] -> Some Login
79 | | [ "access-denied" ] -> Some AccessDenied
80 | | [ "private"; "listing" ] -> Some Listing
81 | | [ "private"; "form"; code ] -> Some (Form (int64 code))
82 | | _ -> None
83 |
84 | Router.Create link route
85 |
86 | ```
87 |
88 | The ` SiteRouter ` value has two functions named link and route. The link function is responsible for mapping the **EndPoint** to the URL path, while the route function maps the path back to the **EndPoint**.
89 |
90 | The last line, creates a Router using the WebSharper function `Router.Create`, using both functions `link` and `route`. There are several options for build customized routes, including the ` Router.CreateWithQuery ` function, which might be useful when you need to setup dynamic URLs.
91 |
92 | This routing model allows full control over the routing options.
93 |
94 | Finally, we need to install the router, so WebSharper can use it. Notice that we turn off the `AccessDenied` **EndPoint** by using the `Router.Slice` utility function.
95 |
96 | ```fsharp
97 | []
98 | let InstallRouter () =
99 | let router =
100 | SiteRouter
101 | |> Router.Slice
102 | (fun endpoint ->
103 | (* Turn off client side routing for AccessDenied endpoint *)
104 | match endpoint with
105 | | AccessDenied -> None
106 | | _ -> Some endpoint
107 | )
108 | id
109 | |> Router.Install Home
110 | router
111 |
112 | ```
113 |
114 | This function returns a `Var` value, which allows us to navigate among the EndPoints. The ` Router.Install ` function is responsible for building such object.
115 |
116 | Note: **Var** refers to the Reactive Variable type provided by WebSharper and will be introduced later in this tutorial.
117 |
118 | The last step here is to replace the `EndPoint` type in the ***Main.fs*** file by this new one:
119 |
120 | ```fsharp
121 | // Delete this type
122 | type EndPoint =
123 | | [] Home
124 |
125 | // open the Routes namespace
126 | module Site =
127 | open WebSharper.UI.Html
128 | open WebSharperTutorial.FrontEnd.Routes // <-- add this line
129 | ...
130 |
131 | // and replace the Main value by this one
132 | []
133 | let Main =
134 | Sitelet.New
135 | SiteRouter
136 | (fun ctx endpoint ->
137 | match endpoint with
138 | | EndPoint.Home -> HomePage ctx
139 | | _ ->
140 | MainTemplate ctx EndPoint.Home "not implemented"
141 | [ div [] [ text "implementation pending" ] ]
142 | )
143 |
144 | ```
145 |
146 | You might want to build an run the project to test this changes. Try other route to see if it works (e.g. ).
147 |
148 | ## embedding resources
149 |
150 | The basic template references some external ***.css*** and ***.js*** files. In this section, we are going to replace them by WebSharper's resource system.
151 |
152 | Let's create a new file named ***Resources.fs*** and add to the ***.fsproj*** file, as usual.
153 |
154 | For now, we are going to embed only the Bootstrap files. Also, we are going to change the default path to JQuery library, which is used by WebSharper framework (pending: add app.config and change JQuery's path).
155 |
156 | First, the Bootstrap files.
157 |
158 | Download the Bootstrap bundle file from the their website and place the dist content at `/wwwroot/vendor/bootstrap/` directory.
159 |
160 | Now, add the following code to the ***Resources.fs*** file:
161 |
162 | ```fsharp
163 | namespace WebSharperTutorial.FrontEnd
164 |
165 | open System
166 | open WebSharper
167 | open WebSharper.Resources
168 |
169 | module AppResources =
170 |
171 | module Bootstrap =
172 | [)>]
173 | type Js() =
174 | inherit BaseResource("/vendor/bootstrap/js/bootstrap.bundle.min.js")
175 | type Css() =
176 | inherit BaseResource("/vendor/bootstrap/css/bootstrap.min.css")
177 |
178 | module FrontEndApp =
179 | type Css() =
180 | inherit BaseResource("/app/css/common.css")
181 |
182 | type Js() =
183 | inherit BaseResource("/app/js/common.js")
184 |
185 | [);
186 | assembly:Require(typeof);
187 | assembly:Require(typeof);
188 | assembly:Require(typeof);
189 | >]
190 | do()
191 |
192 | ```
193 |
194 | Notice that we also created a resource (`FrontEndApp`) for Javascript and stylesheet used by our application. This files must be created at `/wwwroot/app/`.
195 |
196 | The last step is to remove the reference from the ***template/Main.html*** file
197 |
198 | ```html
199 |
200 |
201 |
202 |
203 |
204 | ...
205 |
206 |
207 | ```
208 |
209 | Let's test it. Build and run the application again and check the source code with the browser Inspector.
210 |
211 | > Note: you might need to run "dotnet clean" before build it, to get the template page updated.
212 |
213 | If you check the browser inspector, you will notice both Bootstrap and application ***.css*** and ***.fs*** files wheren't loaded.
214 |
215 | 
216 |
217 | **This is a very important point about how WebSharper client code works**: these resources won't be loaded until any WebSharper's client code is invoked. And you as might recall, we only render a static page built on the server, until now.
218 |
219 | To make it work, change the following line in the ***Main.fs*** file and rebuild the solution again.
220 |
221 | ```fsharp
222 | ...
223 | let HomePage ctx =
224 | MainTemplate ctx EndPoint.Home "Home" [
225 | h1 [] [text "It works!"]
226 | client <@ div [] [ text "Hi there!" ] @>
227 | ]
228 | ...
229 | ```
230 |
231 | Rebuild the project and open it on the browser again. Now you might see the ***.css*** and ***.fs*** resources with the browser Inspector.
232 |
233 | 
234 |
235 | | [previous](./cookbook-chapter-01.md) | [up](../README.md) | [Chapter 03 - ASP.NET Authentication](./cookbook-chapter-03.md) |
236 |
--------------------------------------------------------------------------------
/articles/cookbook-chapter-05.md:
--------------------------------------------------------------------------------
1 | - [Chapter 05 - The listing page](#sec-1)
2 |
3 | # TODO Chapter 05 - The listing page
4 |
5 | We are almost done. We have all the logic for routing, navigation and authentication put together. Now, it is time to create a few pages to emulate a data driven application.
6 |
7 | The first task is to build a listing page. This page will make a remote call to the server to get some data and render it as a HTML table.
8 |
9 | For each line, we are going to add a link to redirect the page to the form passing the respective record's code, so we can open it from there.
10 |
11 | 
12 |
13 | Add two new files to the project:
14 |
15 | - templates/Page.Listing.html
16 | - Page.Listing.fs
17 |
18 | For the template one, add the following code snippet:
19 |
20 | ```html
21 |
22 |
23 |
24 | #
25 | First
26 | Last
27 | Updated At
28 |
29 |
30 |
31 |
32 | ${Code}
33 | ${Firstname}
34 | ${Lastname}
35 | ${UpdatedAt}
36 |
37 |
38 |
39 |
40 | ```
41 |
42 | This snippet introduces a new attribute named `ws-template`. This attribute defines a inner template, which can be instantiated in F# code and composed with *Doc* elements as usual.
43 |
44 | For the ***Page.Listing.fs*** file:
45 |
46 | ```fsharp
47 | namespace WebSharperTutorial.FrontEnd.Pages
48 |
49 | open WebSharper
50 | open WebSharper.UI
51 | open WebSharper.UI.Client
52 | open WebSharper.UI.Html
53 |
54 | open WebSharperTutorial.FrontEnd
55 | open WebSharperTutorial.FrontEnd.Components
56 |
57 | []
58 | module PageListing =
59 |
60 | type private listingTemplate = Templating.Template<"templates/Page.Listing.html">
61 |
62 | let private buildTable router (users:DTO.User list) =
63 | let tableRows =
64 | users
65 | |> List.map(fun user ->
66 | listingTemplate.RowTemplate()
67 | .Code(string user.Code)
68 | .Firstname(user.Firstname)
69 | .Lastname(user.Lastname)
70 | .UpdatedAt(user.UpdateDate.ToShortDateString())
71 | .OnEdit(fun _ -> Var.Set router (Routes.Form user.Code))
72 | .Doc()
73 | )
74 |
75 | listingTemplate()
76 | .Rows(tableRows)
77 | .Doc()
78 |
79 | let Main router =
80 | async {
81 | let navBar =
82 | NavigationBar.Main router
83 |
84 | let! users =
85 | Server.GetUsers()
86 |
87 | let tableElement =
88 | buildTable router users
89 |
90 | return
91 | [
92 | navBar
93 | div [ attr.``class`` "container" ]
94 | [
95 | div [ attr.``class`` "row" ]
96 | [ div [ attr.``class`` "col-12" ]
97 | [ tableElement ]
98 | ]
99 | ]
100 | ]
101 | |> Doc.Concat
102 | }
103 | |> Doc.Async
104 | ```
105 |
106 | This page is getting data from the server through a RPC request and is rendering a table with its content.
107 |
108 | For each line, it instantiate the `listingTemplate.RowTemplate` class and fill in its hole, while setup the `OnEdit` event, aswell.
109 |
110 | The RPC function is asynchrounous, than it requires the `async` computation expression. The returning result is passed to the `Doc.Async` function which will start the async block and return a *Doc* abstraction as the final result.
111 |
112 | For the Server side logic, let's add two new files to the project:
113 |
114 | - DTO.fs
115 | - Server.fs
116 |
117 | as following:
118 |
119 | ```fsharp
120 | namespace WebSharperTutorial.FrontEnd
121 |
122 | open System
123 |
124 | open WebSharper
125 |
126 | []
127 | module DTO =
128 |
129 | type User = {
130 | Code: int64
131 | Firstname: string
132 | Lastname: string
133 | UpdateDate: DateTime
134 | }
135 |
136 | let CreateUser code firstname lastname updateDate =
137 | {
138 | Code = code
139 | Firstname = firstname
140 | Lastname = lastname
141 | UpdateDate = updateDate
142 | }
143 |
144 | ```
145 |
146 | This file contains the *Data Transfer Object* (DTO) types used to send and receive data between client and server side. The important aspect here is the use of `[]` attribute, so the WebSharper compiler can transpile it to *Javascript*.
147 |
148 | And for the ***Server.fs*** file:
149 |
150 | ```fsharp
151 | namespace WebSharperTutorial.FrontEnd
152 |
153 | open System
154 |
155 | open WebSharper
156 |
157 | open WebSharperTutorial.FrontEnd
158 | open WebSharperTutorial.FrontEnd.DTO
159 |
160 | module Server =
161 |
162 | let private dbUsers () =
163 | [
164 | CreateUser 1L "Firstname 1" "Lastname 1" (new DateTime(2020,3,17))
165 | CreateUser 2L "Firstname 2" "Lastname 2" (new DateTime(2019,6,21))
166 | CreateUser 3L "Firstname 3" "Lastname 3" (new DateTime(2019,8,14))
167 | ]
168 |
169 | []
170 | let GetUsers () : Async =
171 | async {
172 | return dbUsers()
173 | }
174 |
175 | ```
176 |
177 | We are generating dummy data for testing. The `RPC` attribute instructs *WebSharper*'s compiler to create all the RPC logic for this asynchrounous function, handling the conversion of its result to JSON.
178 |
179 | Finally, edit the ***Main.fs*** file and reference the new listing page:
180 |
181 | ```fsharp
182 | ...
183 | []
184 | let RouteClientPage () =
185 | let router = Routes.InstallRouter ()
186 |
187 | router.View
188 | |> View.Map (fun endpoint ->
189 | match endpoint with
190 | ...
191 | | EndPoint.Listing ->
192 | PageListing.Main router // <-- replaced line
193 | ...
194 | ```
195 |
196 | | [previous](./cookbook-chapter-04.md) | [up](../README.md) | [Chapter 06 - The form page](./cookbook-chapter-06.md) |
197 |
--------------------------------------------------------------------------------
/articles/cookbook-chapter-05.org:
--------------------------------------------------------------------------------
1 | * TODO Chapter 05 - The listing page
2 | We are almost done. We have all the logic for routing, navigation and
3 | authentication put together. Now, it is time to create a few pages to emulate a
4 | data driven application.
5 |
6 | The first task is to build a listing page. This page will make a remote call to
7 | the server to get some data and render it as a HTML table.
8 |
9 | For each line, we are going to add a link to redirect the page to the form
10 | passing the respective record's code, so we can open it from there.
11 |
12 | #+CAPTION: The listing page
13 | #+NAME: fig:WST-PRINT0001
14 | [[./images/cookbook-chapter-05-image-01.png]]
15 |
16 | Add two new files to the project:
17 | - templates/Page.Listing.html
18 | - Page.Listing.fs
19 |
20 | For the template one, add the following code snippet:
21 |
22 | #+BEGIN_SRC html
23 |
24 |
25 |
26 | #
27 | First
28 | Last
29 | Updated At
30 |
31 |
32 |
33 |
34 | ${Code}
35 | ${Firstname}
36 | ${Lastname}
37 | ${UpdatedAt}
38 |
39 |
40 |
41 |
42 | #+END_SRC
43 |
44 | This snippet introduces a new attribute named src_fsharp[:exports code]{ws-template}.
45 | This attribute defines a inner template, which can be instantiated in F# code and
46 | composed with /Doc/ elements as usual.
47 |
48 | For the */Page.Listing.fs/* file:
49 |
50 | #+BEGIN_SRC fsharp
51 | namespace WebSharperTutorial.FrontEnd.Pages
52 |
53 | open WebSharper
54 | open WebSharper.UI
55 | open WebSharper.UI.Client
56 | open WebSharper.UI.Html
57 |
58 | open WebSharperTutorial.FrontEnd
59 | open WebSharperTutorial.FrontEnd.Components
60 |
61 | []
62 | module PageListing =
63 |
64 | type private listingTemplate = Templating.Template<"templates/Page.Listing.html">
65 |
66 | let private buildTable router (users:DTO.User list) =
67 | let tableRows =
68 | users
69 | |> List.map(fun user ->
70 | listingTemplate.RowTemplate()
71 | .Code(string user.Code)
72 | .Firstname(user.Firstname)
73 | .Lastname(user.Lastname)
74 | .UpdatedAt(user.UpdateDate.ToShortDateString())
75 | .OnEdit(fun _ -> Var.Set router (Routes.Form user.Code))
76 | .Doc()
77 | )
78 |
79 | listingTemplate()
80 | .Rows(tableRows)
81 | .Doc()
82 |
83 | let Main router =
84 | async {
85 | let navBar =
86 | NavigationBar.Main router
87 |
88 | let! users =
89 | Server.GetUsers()
90 |
91 | let tableElement =
92 | buildTable router users
93 |
94 | return
95 | [
96 | navBar
97 | div [ attr.``class`` "container" ]
98 | [
99 | div [ attr.``class`` "row" ]
100 | [ div [ attr.``class`` "col-12" ]
101 | [ tableElement ]
102 | ]
103 | ]
104 | ]
105 | |> Doc.Concat
106 | }
107 | |> Doc.Async
108 | #+END_SRC
109 |
110 | This page is getting data from the server through a RPC request and is rendering
111 | a table with its content.
112 |
113 | For each line, it instantiate the src_fsharp[:exports code]{listingTemplate.RowTemplate}
114 | class and fill in its hole, while setup the src_fsharp[:exports code]{OnEdit} event, aswell.
115 |
116 | The RPC function is asynchrounous, than it requires the src_fsharp[:exports code]{async}
117 | computation expression. The returning result is passed to the src_fsharp[:exports code]{Doc.Async}
118 | function which will start the async block and return a /Doc/ abstraction as the final result.
119 |
120 | For the Server side logic, let's add two new files to the project:
121 | - DTO.fs
122 | - Server.fs
123 |
124 | as following:
125 |
126 | #+BEGIN_SRC fsharp
127 | namespace WebSharperTutorial.FrontEnd
128 |
129 | open System
130 |
131 | open WebSharper
132 |
133 | []
134 | module DTO =
135 |
136 | type User = {
137 | Code: int64
138 | Firstname: string
139 | Lastname: string
140 | UpdateDate: DateTime
141 | }
142 |
143 | let CreateUser code firstname lastname updateDate =
144 | {
145 | Code = code
146 | Firstname = firstname
147 | Lastname = lastname
148 | UpdateDate = updateDate
149 | }
150 |
151 | #+END_SRC
152 |
153 | This file contains the /Data Transfer Object/ (DTO) types used to send and receive
154 | data between client and server side. The important aspect here is the
155 | use of src_fsharp[:exports code]{[]} attribute, so the WebSharper compiler
156 | can transpile it to /Javascript/.
157 |
158 | And for the */Server.fs/* file:
159 |
160 | #+BEGIN_SRC fsharp
161 | namespace WebSharperTutorial.FrontEnd
162 |
163 | open System
164 |
165 | open WebSharper
166 |
167 | open WebSharperTutorial.FrontEnd
168 | open WebSharperTutorial.FrontEnd.DTO
169 |
170 | module Server =
171 |
172 | let private dbUsers () =
173 | [
174 | CreateUser 1L "Firstname 1" "Lastname 1" (new DateTime(2020,3,17))
175 | CreateUser 2L "Firstname 2" "Lastname 2" (new DateTime(2019,6,21))
176 | CreateUser 3L "Firstname 3" "Lastname 3" (new DateTime(2019,8,14))
177 | ]
178 |
179 | []
180 | let GetUsers () : Async =
181 | async {
182 | return dbUsers()
183 | }
184 |
185 | #+END_SRC
186 |
187 | We are generating dummy data for testing. The src_fsharp[:exports code]{RPC} attribute
188 | instructs /WebSharper/'s compiler to create all the RPC logic for this asynchrounous
189 | function, handling the conversion of its result to JSON.
190 |
191 | Finally, edit the */Main.fs/* file and reference the new listing page:
192 |
193 | #+BEGIN_SRC fsharp
194 | ...
195 | []
196 | let RouteClientPage () =
197 | let router = Routes.InstallRouter ()
198 |
199 | router.View
200 | |> View.Map (fun endpoint ->
201 | match endpoint with
202 | ...
203 | | EndPoint.Listing ->
204 | PageListing.Main router // <-- replaced line
205 | ...
206 | #+END_SRC
207 |
208 |
209 | |----------+----+----------------------------|
210 | | [[./cookbook-chapter-04.org][previous]] | [[../README.md][up]] | [[./cookbook-chapter-06.org][Chapter 06 - The form page]] |
211 | |----------+----+----------------------------|
212 |
--------------------------------------------------------------------------------
/articles/cookbook-introduction.md:
--------------------------------------------------------------------------------
1 | - [Introduction](#sec-1)
2 |
3 | # Introduction
4 |
5 | This tutorial will guide you throughout the programming of a simple web application using WebSharper framework.
6 |
7 | In this tutorial, we are going to build a web application using the WebSharper framework. The final application is a mix of server-side and client-side implementation with authentication and basic listing and CRUD features.
8 |
9 | The topics we are about to cover are:
10 |
11 | - authentication: a basic Forms Authentication implementation
12 | - routes: server and client side
13 | - resources: how to include resources like .js and .css files
14 | - reactive variables: how to make dynamic UI
15 | - templates: composition using templates
16 | - extension: how to create a simple extension using WIG language (pending)
17 |
18 | We are going to use WebSharper 4.6 with .NET Core 3.1, and mostly the command line to setup and run the application.
19 |
20 | | [up](../README.md) | [Chapter 01 - Foundations](./cookbook-chapter-01.md) |
21 |
--------------------------------------------------------------------------------
/articles/cookbook-introduction.org:
--------------------------------------------------------------------------------
1 | * Introduction
2 | This tutorial will guide you throughout the programming of a simple web
3 | application using WebSharper framework.
4 |
5 | In this tutorial, we are going to build a web application using the WebSharper
6 | framework. The final application is a mix of server-side and client-side
7 | implementation with authentication and basic listing and CRUD features.
8 |
9 | The topics we are about to cover are:
10 | - authentication: a basic Forms Authentication implementation
11 | - routes: server and client side
12 | - resources: how to include resources like .js and .css files
13 | - reactive variables: how to make dynamic UI
14 | - templates: composition using templates
15 | - extension: how to create a simple extension using WIG language (pending)
16 |
17 | We are going to use WebSharper 4.6 with .NET Core 3.1, and mostly the command
18 | line to setup and run the application.
19 |
20 | |----+--------------------------|
21 | | [[../README.md][up]] | [[./cookbook-chapter-01.org][Chapter 01 - Foundations]] |
22 | |----+--------------------------|
23 |
24 |
--------------------------------------------------------------------------------
/articles/images/cookbook-chapter-01-image-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/articles/images/cookbook-chapter-01-image-01.png
--------------------------------------------------------------------------------
/articles/images/cookbook-chapter-02-image-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/articles/images/cookbook-chapter-02-image-01.png
--------------------------------------------------------------------------------
/articles/images/cookbook-chapter-02-image-02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/articles/images/cookbook-chapter-02-image-02.png
--------------------------------------------------------------------------------
/articles/images/cookbook-chapter-02-image-03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/articles/images/cookbook-chapter-02-image-03.png
--------------------------------------------------------------------------------
/articles/images/cookbook-chapter-03-image-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/articles/images/cookbook-chapter-03-image-01.png
--------------------------------------------------------------------------------
/articles/images/cookbook-chapter-04-image-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/articles/images/cookbook-chapter-04-image-01.png
--------------------------------------------------------------------------------
/articles/images/cookbook-chapter-05-image-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/articles/images/cookbook-chapter-05-image-01.png
--------------------------------------------------------------------------------
/articles/images/cookbook-chapter-06-image-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/articles/images/cookbook-chapter-06-image-01.png
--------------------------------------------------------------------------------
/articles/images/websharper-default-project.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/articles/images/websharper-default-project.png
--------------------------------------------------------------------------------
/code/chapter-01/WebSharperTutorial.FrontEnd/Main.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open WebSharper
4 | open WebSharper.Sitelets
5 | open WebSharper.UI
6 | open WebSharper.UI.Server
7 |
8 | type EndPoint =
9 | | [] Home
10 |
11 | module Site =
12 | open WebSharper.UI.Html
13 |
14 | type MainTemplate = Templating.Template<"templates/Main.html">
15 |
16 | let private MainTemplate ctx action (title: string) (body: Doc list) =
17 | Content.Page(
18 | MainTemplate()
19 | .Title(title)
20 | .Body(body)
21 | .Doc()
22 | )
23 |
24 | let HomePage ctx =
25 | MainTemplate ctx EndPoint.Home "Home" [
26 | h1 [] [text "It works!"]
27 | div [] [ text "Hi there!" ]
28 | ]
29 |
30 | []
31 | let Main =
32 | Application.MultiPage (fun ctx endpoint ->
33 | match endpoint with
34 | | EndPoint.Home -> HomePage ctx
35 | )
36 |
37 |
--------------------------------------------------------------------------------
/code/chapter-01/WebSharperTutorial.FrontEnd/Startup.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 | open Microsoft.AspNetCore
5 | open Microsoft.AspNetCore.Builder
6 | open Microsoft.AspNetCore.Hosting
7 | open Microsoft.AspNetCore.Http
8 | open Microsoft.Extensions.Configuration
9 | open Microsoft.Extensions.DependencyInjection
10 | open Microsoft.Extensions.Hosting
11 | open WebSharper.AspNetCore
12 |
13 | type Startup() =
14 |
15 | member this.ConfigureServices(services: IServiceCollection) =
16 | services.AddSitelet(Site.Main)
17 | .AddAuthentication("WebSharper")
18 | .AddCookie("WebSharper", fun options -> ())
19 | |> ignore
20 |
21 | member this.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =
22 | if env.IsDevelopment() then app.UseDeveloperExceptionPage() |> ignore
23 |
24 | app.UseAuthentication()
25 | .UseStaticFiles()
26 | .UseWebSharper()
27 | .Run(fun context ->
28 | context.Response.StatusCode <- 404
29 | context.Response.WriteAsync("Page not found"))
30 |
31 | module Program =
32 | let BuildWebHost args =
33 | WebHost
34 | .CreateDefaultBuilder(args)
35 | .UseStartup()
36 | .Build()
37 |
38 | []
39 | let main args =
40 | BuildWebHost(args).Run()
41 | 0
42 |
--------------------------------------------------------------------------------
/code/chapter-01/WebSharperTutorial.FrontEnd/WebSharperTutorial.FrontEnd.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/code/chapter-01/WebSharperTutorial.FrontEnd/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "websharper": {
3 | "WebSharper.JQuery.Resources.JQuery": "https://code.jquery.com/jquery-3.2.1.min.js"
4 | }
5 | }
--------------------------------------------------------------------------------
/code/chapter-01/WebSharperTutorial.FrontEnd/templates/Main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | ${Title}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/code/chapter-01/WebSharperTutorial.FrontEnd/wsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://websharper.com/wsconfig.schema.json",
3 | "project": "site",
4 | "outputDir": "wwwroot"
5 | }
6 |
--------------------------------------------------------------------------------
/code/chapter-01/WebSharperTutorial.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26124.0
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WebSharperTutorial.FrontEnd", "WebSharperTutorial.FrontEnd\WebSharperTutorial.FrontEnd.fsproj", "{670A967F-F450-499E-BD99-9F13DB3B5E06}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Debug|x64 = Debug|x64
12 | Debug|x86 = Debug|x86
13 | Release|Any CPU = Release|Any CPU
14 | Release|x64 = Release|x64
15 | Release|x86 = Release|x86
16 | EndGlobalSection
17 | GlobalSection(SolutionProperties) = preSolution
18 | HideSolutionNode = FALSE
19 | EndGlobalSection
20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
21 | {670A967F-F450-499E-BD99-9F13DB3B5E06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
22 | {670A967F-F450-499E-BD99-9F13DB3B5E06}.Debug|Any CPU.Build.0 = Debug|Any CPU
23 | {670A967F-F450-499E-BD99-9F13DB3B5E06}.Debug|x64.ActiveCfg = Debug|Any CPU
24 | {670A967F-F450-499E-BD99-9F13DB3B5E06}.Debug|x64.Build.0 = Debug|Any CPU
25 | {670A967F-F450-499E-BD99-9F13DB3B5E06}.Debug|x86.ActiveCfg = Debug|Any CPU
26 | {670A967F-F450-499E-BD99-9F13DB3B5E06}.Debug|x86.Build.0 = Debug|Any CPU
27 | {670A967F-F450-499E-BD99-9F13DB3B5E06}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {670A967F-F450-499E-BD99-9F13DB3B5E06}.Release|Any CPU.Build.0 = Release|Any CPU
29 | {670A967F-F450-499E-BD99-9F13DB3B5E06}.Release|x64.ActiveCfg = Release|Any CPU
30 | {670A967F-F450-499E-BD99-9F13DB3B5E06}.Release|x64.Build.0 = Release|Any CPU
31 | {670A967F-F450-499E-BD99-9F13DB3B5E06}.Release|x86.ActiveCfg = Release|Any CPU
32 | {670A967F-F450-499E-BD99-9F13DB3B5E06}.Release|x86.Build.0 = Release|Any CPU
33 | EndGlobalSection
34 | EndGlobal
35 |
--------------------------------------------------------------------------------
/code/chapter-02/WebSharperTutorial.FrontEnd/Main.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open WebSharper
4 | open WebSharper.Sitelets
5 | open WebSharper.UI
6 | open WebSharper.UI.Server
7 |
8 | module Site =
9 | open WebSharper.UI.Html
10 | open WebSharperTutorial.FrontEnd.Routes
11 |
12 | type MainTemplate = Templating.Template<"templates/Main.html">
13 |
14 | let private MainTemplate ctx action (title: string) (body: Doc list) =
15 | Content.Page(
16 | MainTemplate()
17 | .Title(title)
18 | .Body(body)
19 | .Doc()
20 | )
21 |
22 | let HomePage ctx =
23 | MainTemplate ctx EndPoint.Home "Home" [
24 | h1 [] [text "It works!"]
25 | client <@ div [] [ text "Hi there!" ] @>
26 | ]
27 |
28 | []
29 | let Main =
30 | Sitelet.New
31 | SiteRouter
32 | (fun ctx endpoint ->
33 | match endpoint with
34 | | EndPoint.Home -> HomePage ctx
35 | | _ ->
36 | MainTemplate ctx EndPoint.Home "not implemented"
37 | [ div [] [ text "implementation pending" ] ]
38 | )
39 |
--------------------------------------------------------------------------------
/code/chapter-02/WebSharperTutorial.FrontEnd/Resources.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 | open WebSharper
5 | open WebSharper.Resources
6 |
7 | module AppResources =
8 |
9 | module Bootstrap =
10 | [)>]
11 | type Js() =
12 | inherit BaseResource("/vendor/bootstrap/js/bootstrap.bundle.min.js")
13 | type Css() =
14 | inherit BaseResource("/vendor/bootstrap/css/bootstrap.min.css")
15 |
16 | module FrontEndApp =
17 | type Css() =
18 | inherit BaseResource("/app/css/common.css")
19 |
20 | type Js() =
21 | inherit BaseResource("/app/js/common.js")
22 |
23 | [);
24 | assembly:Require(typeof);
25 | assembly:Require(typeof);
26 | assembly:Require(typeof);
27 | >]
28 | do()
29 |
--------------------------------------------------------------------------------
/code/chapter-02/WebSharperTutorial.FrontEnd/Routes.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 | open WebSharper
5 | open WebSharper.Sitelets
6 | open WebSharper.UI
7 |
8 | module Routes =
9 |
10 | []
11 | type EndPoint =
12 | | [] Home
13 | | [] Login
14 | | [] AccessDenied
15 | | [] Listing
16 | | [] Form of int64
17 |
18 | (* Router is used by both client and server side *)
19 | []
20 | let SiteRouter : Router =
21 | let link endPoint =
22 | match endPoint with
23 | | Home -> [ ]
24 | | Login -> [ "login" ]
25 | | AccessDenied -> [ "access-denied" ]
26 | | Listing -> [ "private"; "listing" ]
27 | | Form code -> [ "private"; "form"; string code ]
28 |
29 | let route (path) =
30 | match path with
31 | | [ ] -> Some Home
32 | | [ "login" ] -> Some Login
33 | | [ "access-denied" ] -> Some AccessDenied
34 | | [ "private"; "listing" ] -> Some Listing
35 | | [ "private"; "form"; code ] -> Some (Form (int64 code))
36 | | _ -> None
37 |
38 | Router.Create link route
39 |
40 | []
41 | let InstallRouter () =
42 | let router =
43 | SiteRouter
44 | |> Router.Slice
45 | (fun endpoint ->
46 | (* Turn off client side routing for AccessDenied endpoint *)
47 | match endpoint with
48 | | AccessDenied -> None
49 | | _ -> Some endpoint
50 | )
51 | id
52 | |> Router.Install Home
53 | router
54 |
--------------------------------------------------------------------------------
/code/chapter-02/WebSharperTutorial.FrontEnd/Startup.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 | open Microsoft.AspNetCore
5 | open Microsoft.AspNetCore.Builder
6 | open Microsoft.AspNetCore.Hosting
7 | open Microsoft.AspNetCore.Http
8 | open Microsoft.Extensions.Configuration
9 | open Microsoft.Extensions.DependencyInjection
10 | open Microsoft.Extensions.Hosting
11 | open WebSharper.AspNetCore
12 |
13 | type Startup() =
14 |
15 | member this.ConfigureServices(services: IServiceCollection) =
16 | services.AddSitelet(Site.Main)
17 | .AddAuthentication("WebSharper")
18 | .AddCookie("WebSharper", fun options -> ())
19 | |> ignore
20 |
21 | member this.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =
22 | if env.IsDevelopment() then app.UseDeveloperExceptionPage() |> ignore
23 |
24 | app.UseAuthentication()
25 | .UseStaticFiles()
26 | .UseWebSharper()
27 | .Run(fun context ->
28 | context.Response.StatusCode <- 404
29 | context.Response.WriteAsync("Page not found"))
30 |
31 | module Program =
32 | let BuildWebHost args =
33 | WebHost
34 | .CreateDefaultBuilder(args)
35 | .UseStartup()
36 | .Build()
37 |
38 | []
39 | let main args =
40 | BuildWebHost(args).Run()
41 | 0
42 |
--------------------------------------------------------------------------------
/code/chapter-02/WebSharperTutorial.FrontEnd/WebSharperTutorial.FrontEnd.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/code/chapter-02/WebSharperTutorial.FrontEnd/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "websharper": {
3 | "WebSharper.JQuery.Resources.JQuery": "https://code.jquery.com/jquery-3.2.1.min.js"
4 | }
5 | }
--------------------------------------------------------------------------------
/code/chapter-02/WebSharperTutorial.FrontEnd/templates/Main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | ${Title}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/code/chapter-02/WebSharperTutorial.FrontEnd/wsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://websharper.com/wsconfig.schema.json",
3 | "project": "site",
4 | "outputDir": "wwwroot"
5 | }
6 |
--------------------------------------------------------------------------------
/code/chapter-02/WebSharperTutorial.FrontEnd/wwwroot/app/css/common.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/code/chapter-02/WebSharperTutorial.FrontEnd/wwwroot/app/css/common.css
--------------------------------------------------------------------------------
/code/chapter-02/WebSharperTutorial.FrontEnd/wwwroot/app/js/common.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/code/chapter-02/WebSharperTutorial.FrontEnd/wwwroot/app/js/common.js
--------------------------------------------------------------------------------
/code/chapter-02/WebSharperTutorial.FrontEnd/wwwroot/vendor/bootstrap/css/bootstrap-reboot.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v4.5.0 (https://getbootstrap.com/)
3 | * Copyright 2011-2020 The Bootstrap Authors
4 | * Copyright 2011-2020 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */
8 | *,
9 | *::before,
10 | *::after {
11 | box-sizing: border-box;
12 | }
13 |
14 | html {
15 | font-family: sans-serif;
16 | line-height: 1.15;
17 | -webkit-text-size-adjust: 100%;
18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
19 | }
20 |
21 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
22 | display: block;
23 | }
24 |
25 | body {
26 | margin: 0;
27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
28 | font-size: 1rem;
29 | font-weight: 400;
30 | line-height: 1.5;
31 | color: #212529;
32 | text-align: left;
33 | background-color: #fff;
34 | }
35 |
36 | [tabindex="-1"]:focus:not(:focus-visible) {
37 | outline: 0 !important;
38 | }
39 |
40 | hr {
41 | box-sizing: content-box;
42 | height: 0;
43 | overflow: visible;
44 | }
45 |
46 | h1, h2, h3, h4, h5, h6 {
47 | margin-top: 0;
48 | margin-bottom: 0.5rem;
49 | }
50 |
51 | p {
52 | margin-top: 0;
53 | margin-bottom: 1rem;
54 | }
55 |
56 | abbr[title],
57 | abbr[data-original-title] {
58 | text-decoration: underline;
59 | -webkit-text-decoration: underline dotted;
60 | text-decoration: underline dotted;
61 | cursor: help;
62 | border-bottom: 0;
63 | -webkit-text-decoration-skip-ink: none;
64 | text-decoration-skip-ink: none;
65 | }
66 |
67 | address {
68 | margin-bottom: 1rem;
69 | font-style: normal;
70 | line-height: inherit;
71 | }
72 |
73 | ol,
74 | ul,
75 | dl {
76 | margin-top: 0;
77 | margin-bottom: 1rem;
78 | }
79 |
80 | ol ol,
81 | ul ul,
82 | ol ul,
83 | ul ol {
84 | margin-bottom: 0;
85 | }
86 |
87 | dt {
88 | font-weight: 700;
89 | }
90 |
91 | dd {
92 | margin-bottom: .5rem;
93 | margin-left: 0;
94 | }
95 |
96 | blockquote {
97 | margin: 0 0 1rem;
98 | }
99 |
100 | b,
101 | strong {
102 | font-weight: bolder;
103 | }
104 |
105 | small {
106 | font-size: 80%;
107 | }
108 |
109 | sub,
110 | sup {
111 | position: relative;
112 | font-size: 75%;
113 | line-height: 0;
114 | vertical-align: baseline;
115 | }
116 |
117 | sub {
118 | bottom: -.25em;
119 | }
120 |
121 | sup {
122 | top: -.5em;
123 | }
124 |
125 | a {
126 | color: #007bff;
127 | text-decoration: none;
128 | background-color: transparent;
129 | }
130 |
131 | a:hover {
132 | color: #0056b3;
133 | text-decoration: underline;
134 | }
135 |
136 | a:not([href]) {
137 | color: inherit;
138 | text-decoration: none;
139 | }
140 |
141 | a:not([href]):hover {
142 | color: inherit;
143 | text-decoration: none;
144 | }
145 |
146 | pre,
147 | code,
148 | kbd,
149 | samp {
150 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
151 | font-size: 1em;
152 | }
153 |
154 | pre {
155 | margin-top: 0;
156 | margin-bottom: 1rem;
157 | overflow: auto;
158 | -ms-overflow-style: scrollbar;
159 | }
160 |
161 | figure {
162 | margin: 0 0 1rem;
163 | }
164 |
165 | img {
166 | vertical-align: middle;
167 | border-style: none;
168 | }
169 |
170 | svg {
171 | overflow: hidden;
172 | vertical-align: middle;
173 | }
174 |
175 | table {
176 | border-collapse: collapse;
177 | }
178 |
179 | caption {
180 | padding-top: 0.75rem;
181 | padding-bottom: 0.75rem;
182 | color: #6c757d;
183 | text-align: left;
184 | caption-side: bottom;
185 | }
186 |
187 | th {
188 | text-align: inherit;
189 | }
190 |
191 | label {
192 | display: inline-block;
193 | margin-bottom: 0.5rem;
194 | }
195 |
196 | button {
197 | border-radius: 0;
198 | }
199 |
200 | button:focus {
201 | outline: 1px dotted;
202 | outline: 5px auto -webkit-focus-ring-color;
203 | }
204 |
205 | input,
206 | button,
207 | select,
208 | optgroup,
209 | textarea {
210 | margin: 0;
211 | font-family: inherit;
212 | font-size: inherit;
213 | line-height: inherit;
214 | }
215 |
216 | button,
217 | input {
218 | overflow: visible;
219 | }
220 |
221 | button,
222 | select {
223 | text-transform: none;
224 | }
225 |
226 | [role="button"] {
227 | cursor: pointer;
228 | }
229 |
230 | select {
231 | word-wrap: normal;
232 | }
233 |
234 | button,
235 | [type="button"],
236 | [type="reset"],
237 | [type="submit"] {
238 | -webkit-appearance: button;
239 | }
240 |
241 | button:not(:disabled),
242 | [type="button"]:not(:disabled),
243 | [type="reset"]:not(:disabled),
244 | [type="submit"]:not(:disabled) {
245 | cursor: pointer;
246 | }
247 |
248 | button::-moz-focus-inner,
249 | [type="button"]::-moz-focus-inner,
250 | [type="reset"]::-moz-focus-inner,
251 | [type="submit"]::-moz-focus-inner {
252 | padding: 0;
253 | border-style: none;
254 | }
255 |
256 | input[type="radio"],
257 | input[type="checkbox"] {
258 | box-sizing: border-box;
259 | padding: 0;
260 | }
261 |
262 | textarea {
263 | overflow: auto;
264 | resize: vertical;
265 | }
266 |
267 | fieldset {
268 | min-width: 0;
269 | padding: 0;
270 | margin: 0;
271 | border: 0;
272 | }
273 |
274 | legend {
275 | display: block;
276 | width: 100%;
277 | max-width: 100%;
278 | padding: 0;
279 | margin-bottom: .5rem;
280 | font-size: 1.5rem;
281 | line-height: inherit;
282 | color: inherit;
283 | white-space: normal;
284 | }
285 |
286 | progress {
287 | vertical-align: baseline;
288 | }
289 |
290 | [type="number"]::-webkit-inner-spin-button,
291 | [type="number"]::-webkit-outer-spin-button {
292 | height: auto;
293 | }
294 |
295 | [type="search"] {
296 | outline-offset: -2px;
297 | -webkit-appearance: none;
298 | }
299 |
300 | [type="search"]::-webkit-search-decoration {
301 | -webkit-appearance: none;
302 | }
303 |
304 | ::-webkit-file-upload-button {
305 | font: inherit;
306 | -webkit-appearance: button;
307 | }
308 |
309 | output {
310 | display: inline-block;
311 | }
312 |
313 | summary {
314 | display: list-item;
315 | cursor: pointer;
316 | }
317 |
318 | template {
319 | display: none;
320 | }
321 |
322 | [hidden] {
323 | display: none !important;
324 | }
325 | /*# sourceMappingURL=bootstrap-reboot.css.map */
--------------------------------------------------------------------------------
/code/chapter-02/WebSharperTutorial.FrontEnd/wwwroot/vendor/bootstrap/css/bootstrap-reboot.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v4.5.0 (https://getbootstrap.com/)
3 | * Copyright 2011-2020 The Bootstrap Authors
4 | * Copyright 2011-2020 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */
--------------------------------------------------------------------------------
/code/chapter-02/WebSharperTutorial.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26124.0
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WebSharperTutorial.FrontEnd", "WebSharperTutorial.FrontEnd\WebSharperTutorial.FrontEnd.fsproj", "{6870CF92-C520-4FC9-8814-C93AECA5F5D6}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Debug|x64 = Debug|x64
12 | Debug|x86 = Debug|x86
13 | Release|Any CPU = Release|Any CPU
14 | Release|x64 = Release|x64
15 | Release|x86 = Release|x86
16 | EndGlobalSection
17 | GlobalSection(SolutionProperties) = preSolution
18 | HideSolutionNode = FALSE
19 | EndGlobalSection
20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
21 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
22 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
23 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x64.ActiveCfg = Debug|Any CPU
24 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x64.Build.0 = Debug|Any CPU
25 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x86.ActiveCfg = Debug|Any CPU
26 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x86.Build.0 = Debug|Any CPU
27 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|Any CPU.Build.0 = Release|Any CPU
29 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x64.ActiveCfg = Release|Any CPU
30 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x64.Build.0 = Release|Any CPU
31 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x86.ActiveCfg = Release|Any CPU
32 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x86.Build.0 = Release|Any CPU
33 | EndGlobalSection
34 | EndGlobal
35 |
--------------------------------------------------------------------------------
/code/chapter-03/WebSharperTutorial.FrontEnd/Auth.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | module Auth =
4 | open System
5 |
6 | open WebSharper
7 | open WebSharper.Web
8 | open WebSharper.AspNetCore
9 | open Microsoft.AspNetCore.Identity
10 |
11 | let GetLoggedInUser () =
12 | let ctx = Remoting.GetContext()
13 | ctx.UserSession.GetLoggedInUser()
14 |
15 | []
16 | type RpcUserSession() =
17 | []
18 | abstract GetLogin : unit -> Async>
19 | []
20 | abstract Login : login: string -> Async
21 | []
22 | abstract Logout : unit -> Async
23 | []
24 | abstract CheckCredentials : string -> string -> bool -> Async>
25 |
26 |
27 | []
28 | type ApplicationUser() =
29 | inherit IdentityUser()
30 |
31 | //type RpcUserSessionImpl(dbContext: Database.AppDbContext) =
32 | type RpcUserSessionImpl() =
33 | inherit RpcUserSession()
34 |
35 | let canGetLogged login password =
36 | login = "admin" && password = "admin"
37 |
38 | override this.GetLogin() =
39 | WebSharper.Web.Remoting.GetContext().UserSession.GetLoggedInUser()
40 |
41 | override this.Login(login: string) =
42 | //Validate email...
43 | WebSharper.Web.Remoting.GetContext().UserSession.LoginUser(login)
44 |
45 | override this.Logout() =
46 | WebSharper.Web.Remoting.GetContext().UserSession.Logout()
47 |
48 | override this.CheckCredentials(login:string) (password:string) (keepLogged:bool)
49 | : Async> =
50 | async {
51 | if canGetLogged login password then
52 | do! WebSharper.Web.Remoting.GetContext().UserSession.LoginUser(login)
53 | return Result.Ok "Welcome!"
54 | else
55 | return Result.Error "Invalid credentials."
56 | }
57 |
--------------------------------------------------------------------------------
/code/chapter-03/WebSharperTutorial.FrontEnd/Main.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open WebSharper
4 | open WebSharper.Sitelets
5 | open WebSharper.UI
6 | open WebSharper.UI.Server
7 |
8 | module Site =
9 | open WebSharper.UI.Html
10 | open WebSharper.UI.Client // required by the Doc.EmbedView
11 | open WebSharperTutorial.FrontEnd.Routes
12 | open WebSharperTutorial.FrontEnd.Pages
13 |
14 | type MainTemplate = Templating.Template<"templates/Main.html">
15 |
16 | let private MainTemplate ctx action (title: string) (body: Doc list) =
17 | Content.Page(
18 | MainTemplate()
19 | .Title(title)
20 | .Body(body)
21 | .Doc()
22 | )
23 |
24 | let HomePage ctx =
25 | MainTemplate ctx EndPoint.Home "Home" [
26 | h1 [] [text "It works!"]
27 | client <@ div [] [ text "Hi there!" ] @>
28 | ]
29 |
30 | let LoginPage ctx endpoint =
31 | let body =
32 | client
33 | <@ let router = Routes.InstallRouter ()
34 |
35 | router.View
36 | |> View.Map (fun endpoint ->
37 | PageLogin.Main router
38 | )
39 | |> Doc.EmbedView
40 | @>
41 | MainTemplate ctx endpoint "Login" [ body ]
42 |
43 | []
44 | let Main =
45 | Sitelet.New
46 | SiteRouter
47 | (fun ctx endpoint ->
48 | let loggedUser =
49 | async {
50 | return! ctx.UserSession.GetLoggedInUser()
51 | } |> Async.RunSynchronously
52 |
53 | match loggedUser with
54 | | None -> // user is not authenticated. Allow only public EndPoints
55 | match endpoint with
56 | | EndPoint.Home -> HomePage ctx
57 | | EndPoint.Login ->
58 | LoginPage ctx endpoint
59 | | EndPoint.AccessDenied ->
60 | MainTemplate ctx EndPoint.Home "Access Denied Page"
61 | [ div [] [ text "Access denied" ] ]
62 | | _ ->
63 | Content.RedirectTemporary AccessDenied
64 |
65 | | Some (u) -> // user is authenticated. Allow all EndPoints
66 | match endpoint with
67 | | EndPoint.Home -> HomePage ctx
68 | | EndPoint.Login ->
69 | LoginPage ctx endpoint
70 | | EndPoint.Listing ->
71 | MainTemplate ctx EndPoint.Home "Listing Page"
72 | [ div [] [ text "Listing Page - implementation pending" ] ]
73 | | EndPoint.Form code ->
74 | MainTemplate ctx EndPoint.Home "Form Page"
75 | [ div [] [ text "Form Page - implementation pending" ] ]
76 | | _ ->
77 | MainTemplate ctx EndPoint.Home "not implemented"
78 | [ div [] [ text "implementation pending" ] ]
79 | )
80 |
--------------------------------------------------------------------------------
/code/chapter-03/WebSharperTutorial.FrontEnd/Page.Login.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd.Pages
2 |
3 | open WebSharper
4 | open WebSharper.UI
5 | open WebSharper.UI.Client
6 | open WebSharper.UI.Html
7 | open WebSharper.JQuery
8 | open WebSharper.JavaScript // require by the Remote<'T> type
9 |
10 | open WebSharperTutorial.FrontEnd
11 |
12 | []
13 | module PageLogin =
14 |
15 | type private loginFormTemplate = Templating.Template<"templates/Page.Login.html">
16 |
17 | let private AlertBox (rvStatusMsg:Var) =
18 | rvStatusMsg.View
19 | |> View.Map (fun msgO ->
20 | match msgO with
21 | | None ->
22 | Doc.Empty
23 | | Some msg ->
24 | div [ attr.``class`` "alert alert-primary"
25 | Attr.Create "role" "alert"
26 | ]
27 | [ text msg ]
28 | )
29 | |> Doc.EmbedView
30 |
31 | let private FormLogin (router:Var) =
32 | let rvEmail = Var.Create ""
33 | let rvPassword = Var.Create ""
34 | let rvKeepLogged = Var.Create true
35 | let rvStatusMsg = Var.Create None
36 |
37 | let statusMsgBox = AlertBox rvStatusMsg
38 |
39 | loginFormTemplate()
40 | .AlertBox(statusMsgBox)
41 | .Login(rvEmail)
42 | .Password(rvPassword)
43 | .RememberMe(rvKeepLogged)
44 | .OnLogin(fun _ ->
45 | JQuery.Of("form").One("submit", fun elem ev -> ev.PreventDefault()).Ignore
46 | async {
47 | let! response =
48 | Remote.CheckCredentials rvEmail.Value rvPassword.Value rvKeepLogged.Value
49 | match response with
50 | | Result.Ok c ->
51 | rvEmail.Value <- ""
52 | rvPassword.Value <- ""
53 | rvStatusMsg.Value <- None
54 | router.Value <- Routes.Listing
55 |
56 | | Result.Error error ->
57 | rvStatusMsg.Value <- Some error
58 | }
59 | |> Async.Start
60 | )
61 | .OnLogout(fun _ ->
62 | async {
63 | do! Remote.Logout ()
64 | Var.Set router Routes.Home
65 | }
66 | |> Async.Start
67 | )
68 | .Doc()
69 |
70 | let Main router =
71 | let formLogin = FormLogin router
72 |
73 | div [ attr.``class`` "container" ]
74 | [
75 | div [ attr.``class`` "row" ]
76 | [ div [ attr.``class`` "col-xs-12 col-sm-6 mx-auto" ] [ formLogin ]
77 | ]
78 | ]
79 |
80 |
--------------------------------------------------------------------------------
/code/chapter-03/WebSharperTutorial.FrontEnd/Resources.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 | open WebSharper
5 | open WebSharper.Resources
6 |
7 | module AppResources =
8 |
9 | module Bootstrap =
10 | [)>]
11 | type Js() =
12 | inherit BaseResource("/vendor/bootstrap/js/bootstrap.bundle.min.js")
13 | type Css() =
14 | inherit BaseResource("/vendor/bootstrap/css/bootstrap.min.css")
15 |
16 | module FrontEndApp =
17 | type Css() =
18 | inherit BaseResource("/app/css/common.css")
19 |
20 | type Js() =
21 | inherit BaseResource("/app/js/common.js")
22 |
23 | [);
24 | assembly:Require(typeof);
25 | assembly:Require(typeof);
26 | assembly:Require(typeof);
27 | >]
28 | do()
29 |
--------------------------------------------------------------------------------
/code/chapter-03/WebSharperTutorial.FrontEnd/Routes.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 | open WebSharper
5 | open WebSharper.Sitelets
6 | open WebSharper.UI
7 |
8 | module Routes =
9 |
10 | []
11 | type EndPoint =
12 | | [] Home
13 | | [] Login
14 | | [] AccessDenied
15 | | [] Listing
16 | | [] Form of int64
17 |
18 | (* Router is used by both client and server side *)
19 | []
20 | let SiteRouter : Router =
21 | let link endPoint =
22 | match endPoint with
23 | | Home -> [ ]
24 | | Login -> [ "login" ]
25 | | AccessDenied -> [ "access-denied" ]
26 | | Listing -> [ "private"; "listing" ]
27 | | Form code -> [ "private"; "form"; string code ]
28 |
29 | let route (path) =
30 | match path with
31 | | [ ] -> Some Home
32 | | [ "login" ] -> Some Login
33 | | [ "access-denied" ] -> Some AccessDenied
34 | | [ "private"; "listing" ] -> Some Listing
35 | | [ "private"; "form"; code ] -> Some (Form (int64 code))
36 | | _ -> None
37 |
38 | Router.Create link route
39 |
40 | []
41 | let InstallRouter () =
42 | let router =
43 | SiteRouter
44 | |> Router.Slice
45 | (fun endpoint ->
46 | (* Turn off client side routing for AccessDenied endpoint *)
47 | match endpoint with
48 | | AccessDenied -> None
49 | | _ -> Some endpoint
50 | )
51 | id
52 | |> Router.Install Home
53 | router
54 |
--------------------------------------------------------------------------------
/code/chapter-03/WebSharperTutorial.FrontEnd/Startup.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 | open Microsoft.AspNetCore
5 | open Microsoft.AspNetCore.Builder
6 | open Microsoft.AspNetCore.Hosting
7 | open Microsoft.AspNetCore.Http
8 | open Microsoft.Extensions.Configuration
9 | open Microsoft.Extensions.DependencyInjection
10 | open Microsoft.Extensions.Hosting
11 | open WebSharper.AspNetCore
12 |
13 | type Startup() =
14 |
15 | member this.ConfigureServices(services: IServiceCollection) =
16 | services.AddSitelet(Site.Main)
17 | .AddWebSharperRemoting() // <-- add this line
18 | .AddAuthentication("WebSharper")
19 | .AddCookie("WebSharper", fun options -> ())
20 | |> ignore
21 |
22 | member this.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =
23 | if env.IsDevelopment() then app.UseDeveloperExceptionPage() |> ignore
24 |
25 | app.UseAuthentication()
26 | .UseStaticFiles()
27 | .UseWebSharper()
28 | .Run(fun context ->
29 | context.Response.StatusCode <- 404
30 | context.Response.WriteAsync("Page not found"))
31 |
32 | module Program =
33 | let BuildWebHost args =
34 | WebHost
35 | .CreateDefaultBuilder(args)
36 | .UseStartup()
37 | .Build()
38 |
39 | []
40 | let main args =
41 | BuildWebHost(args).Run()
42 | 0
43 |
--------------------------------------------------------------------------------
/code/chapter-03/WebSharperTutorial.FrontEnd/WebSharperTutorial.FrontEnd.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/code/chapter-03/WebSharperTutorial.FrontEnd/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "websharper": {
3 | "WebSharper.JQuery.Resources.JQuery": "https://code.jquery.com/jquery-3.2.1.min.js"
4 | }
5 | }
--------------------------------------------------------------------------------
/code/chapter-03/WebSharperTutorial.FrontEnd/templates/Main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | ${Title}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/code/chapter-03/WebSharperTutorial.FrontEnd/templates/Page.Login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Provide your credentials
4 |
5 |
6 |
7 |
8 |
28 |
29 |
--------------------------------------------------------------------------------
/code/chapter-03/WebSharperTutorial.FrontEnd/wsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://websharper.com/wsconfig.schema.json",
3 | "project": "site",
4 | "outputDir": "wwwroot"
5 | }
6 |
--------------------------------------------------------------------------------
/code/chapter-03/WebSharperTutorial.FrontEnd/wwwroot/app/css/common.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/code/chapter-03/WebSharperTutorial.FrontEnd/wwwroot/app/css/common.css
--------------------------------------------------------------------------------
/code/chapter-03/WebSharperTutorial.FrontEnd/wwwroot/app/js/common.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/code/chapter-03/WebSharperTutorial.FrontEnd/wwwroot/app/js/common.js
--------------------------------------------------------------------------------
/code/chapter-03/WebSharperTutorial.FrontEnd/wwwroot/vendor/bootstrap/css/bootstrap-reboot.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v4.5.0 (https://getbootstrap.com/)
3 | * Copyright 2011-2020 The Bootstrap Authors
4 | * Copyright 2011-2020 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */
8 | *,
9 | *::before,
10 | *::after {
11 | box-sizing: border-box;
12 | }
13 |
14 | html {
15 | font-family: sans-serif;
16 | line-height: 1.15;
17 | -webkit-text-size-adjust: 100%;
18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
19 | }
20 |
21 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
22 | display: block;
23 | }
24 |
25 | body {
26 | margin: 0;
27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
28 | font-size: 1rem;
29 | font-weight: 400;
30 | line-height: 1.5;
31 | color: #212529;
32 | text-align: left;
33 | background-color: #fff;
34 | }
35 |
36 | [tabindex="-1"]:focus:not(:focus-visible) {
37 | outline: 0 !important;
38 | }
39 |
40 | hr {
41 | box-sizing: content-box;
42 | height: 0;
43 | overflow: visible;
44 | }
45 |
46 | h1, h2, h3, h4, h5, h6 {
47 | margin-top: 0;
48 | margin-bottom: 0.5rem;
49 | }
50 |
51 | p {
52 | margin-top: 0;
53 | margin-bottom: 1rem;
54 | }
55 |
56 | abbr[title],
57 | abbr[data-original-title] {
58 | text-decoration: underline;
59 | -webkit-text-decoration: underline dotted;
60 | text-decoration: underline dotted;
61 | cursor: help;
62 | border-bottom: 0;
63 | -webkit-text-decoration-skip-ink: none;
64 | text-decoration-skip-ink: none;
65 | }
66 |
67 | address {
68 | margin-bottom: 1rem;
69 | font-style: normal;
70 | line-height: inherit;
71 | }
72 |
73 | ol,
74 | ul,
75 | dl {
76 | margin-top: 0;
77 | margin-bottom: 1rem;
78 | }
79 |
80 | ol ol,
81 | ul ul,
82 | ol ul,
83 | ul ol {
84 | margin-bottom: 0;
85 | }
86 |
87 | dt {
88 | font-weight: 700;
89 | }
90 |
91 | dd {
92 | margin-bottom: .5rem;
93 | margin-left: 0;
94 | }
95 |
96 | blockquote {
97 | margin: 0 0 1rem;
98 | }
99 |
100 | b,
101 | strong {
102 | font-weight: bolder;
103 | }
104 |
105 | small {
106 | font-size: 80%;
107 | }
108 |
109 | sub,
110 | sup {
111 | position: relative;
112 | font-size: 75%;
113 | line-height: 0;
114 | vertical-align: baseline;
115 | }
116 |
117 | sub {
118 | bottom: -.25em;
119 | }
120 |
121 | sup {
122 | top: -.5em;
123 | }
124 |
125 | a {
126 | color: #007bff;
127 | text-decoration: none;
128 | background-color: transparent;
129 | }
130 |
131 | a:hover {
132 | color: #0056b3;
133 | text-decoration: underline;
134 | }
135 |
136 | a:not([href]) {
137 | color: inherit;
138 | text-decoration: none;
139 | }
140 |
141 | a:not([href]):hover {
142 | color: inherit;
143 | text-decoration: none;
144 | }
145 |
146 | pre,
147 | code,
148 | kbd,
149 | samp {
150 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
151 | font-size: 1em;
152 | }
153 |
154 | pre {
155 | margin-top: 0;
156 | margin-bottom: 1rem;
157 | overflow: auto;
158 | -ms-overflow-style: scrollbar;
159 | }
160 |
161 | figure {
162 | margin: 0 0 1rem;
163 | }
164 |
165 | img {
166 | vertical-align: middle;
167 | border-style: none;
168 | }
169 |
170 | svg {
171 | overflow: hidden;
172 | vertical-align: middle;
173 | }
174 |
175 | table {
176 | border-collapse: collapse;
177 | }
178 |
179 | caption {
180 | padding-top: 0.75rem;
181 | padding-bottom: 0.75rem;
182 | color: #6c757d;
183 | text-align: left;
184 | caption-side: bottom;
185 | }
186 |
187 | th {
188 | text-align: inherit;
189 | }
190 |
191 | label {
192 | display: inline-block;
193 | margin-bottom: 0.5rem;
194 | }
195 |
196 | button {
197 | border-radius: 0;
198 | }
199 |
200 | button:focus {
201 | outline: 1px dotted;
202 | outline: 5px auto -webkit-focus-ring-color;
203 | }
204 |
205 | input,
206 | button,
207 | select,
208 | optgroup,
209 | textarea {
210 | margin: 0;
211 | font-family: inherit;
212 | font-size: inherit;
213 | line-height: inherit;
214 | }
215 |
216 | button,
217 | input {
218 | overflow: visible;
219 | }
220 |
221 | button,
222 | select {
223 | text-transform: none;
224 | }
225 |
226 | [role="button"] {
227 | cursor: pointer;
228 | }
229 |
230 | select {
231 | word-wrap: normal;
232 | }
233 |
234 | button,
235 | [type="button"],
236 | [type="reset"],
237 | [type="submit"] {
238 | -webkit-appearance: button;
239 | }
240 |
241 | button:not(:disabled),
242 | [type="button"]:not(:disabled),
243 | [type="reset"]:not(:disabled),
244 | [type="submit"]:not(:disabled) {
245 | cursor: pointer;
246 | }
247 |
248 | button::-moz-focus-inner,
249 | [type="button"]::-moz-focus-inner,
250 | [type="reset"]::-moz-focus-inner,
251 | [type="submit"]::-moz-focus-inner {
252 | padding: 0;
253 | border-style: none;
254 | }
255 |
256 | input[type="radio"],
257 | input[type="checkbox"] {
258 | box-sizing: border-box;
259 | padding: 0;
260 | }
261 |
262 | textarea {
263 | overflow: auto;
264 | resize: vertical;
265 | }
266 |
267 | fieldset {
268 | min-width: 0;
269 | padding: 0;
270 | margin: 0;
271 | border: 0;
272 | }
273 |
274 | legend {
275 | display: block;
276 | width: 100%;
277 | max-width: 100%;
278 | padding: 0;
279 | margin-bottom: .5rem;
280 | font-size: 1.5rem;
281 | line-height: inherit;
282 | color: inherit;
283 | white-space: normal;
284 | }
285 |
286 | progress {
287 | vertical-align: baseline;
288 | }
289 |
290 | [type="number"]::-webkit-inner-spin-button,
291 | [type="number"]::-webkit-outer-spin-button {
292 | height: auto;
293 | }
294 |
295 | [type="search"] {
296 | outline-offset: -2px;
297 | -webkit-appearance: none;
298 | }
299 |
300 | [type="search"]::-webkit-search-decoration {
301 | -webkit-appearance: none;
302 | }
303 |
304 | ::-webkit-file-upload-button {
305 | font: inherit;
306 | -webkit-appearance: button;
307 | }
308 |
309 | output {
310 | display: inline-block;
311 | }
312 |
313 | summary {
314 | display: list-item;
315 | cursor: pointer;
316 | }
317 |
318 | template {
319 | display: none;
320 | }
321 |
322 | [hidden] {
323 | display: none !important;
324 | }
325 | /*# sourceMappingURL=bootstrap-reboot.css.map */
--------------------------------------------------------------------------------
/code/chapter-03/WebSharperTutorial.FrontEnd/wwwroot/vendor/bootstrap/css/bootstrap-reboot.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v4.5.0 (https://getbootstrap.com/)
3 | * Copyright 2011-2020 The Bootstrap Authors
4 | * Copyright 2011-2020 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */
--------------------------------------------------------------------------------
/code/chapter-03/WebSharperTutorial.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26124.0
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WebSharperTutorial.FrontEnd", "WebSharperTutorial.FrontEnd\WebSharperTutorial.FrontEnd.fsproj", "{6870CF92-C520-4FC9-8814-C93AECA5F5D6}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Debug|x64 = Debug|x64
12 | Debug|x86 = Debug|x86
13 | Release|Any CPU = Release|Any CPU
14 | Release|x64 = Release|x64
15 | Release|x86 = Release|x86
16 | EndGlobalSection
17 | GlobalSection(SolutionProperties) = preSolution
18 | HideSolutionNode = FALSE
19 | EndGlobalSection
20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
21 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
22 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
23 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x64.ActiveCfg = Debug|Any CPU
24 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x64.Build.0 = Debug|Any CPU
25 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x86.ActiveCfg = Debug|Any CPU
26 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x86.Build.0 = Debug|Any CPU
27 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|Any CPU.Build.0 = Release|Any CPU
29 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x64.ActiveCfg = Release|Any CPU
30 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x64.Build.0 = Release|Any CPU
31 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x86.ActiveCfg = Release|Any CPU
32 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x86.Build.0 = Release|Any CPU
33 | EndGlobalSection
34 | EndGlobal
35 |
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.FrontEnd/Auth.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | module Auth =
4 | open System
5 |
6 | open WebSharper
7 | open WebSharper.Web
8 | open WebSharper.AspNetCore
9 | open Microsoft.AspNetCore.Identity
10 |
11 | let GetLoggedInUser () =
12 | let ctx = Remoting.GetContext()
13 | ctx.UserSession.GetLoggedInUser()
14 |
15 | []
16 | type RpcUserSession() =
17 | []
18 | abstract GetLogin : unit -> Async>
19 | []
20 | abstract Login : login: string -> Async
21 | []
22 | abstract Logout : unit -> Async
23 | []
24 | abstract CheckCredentials : string -> string -> bool -> Async>
25 |
26 |
27 | []
28 | type ApplicationUser() =
29 | inherit IdentityUser()
30 |
31 | //type RpcUserSessionImpl(dbContext: Database.AppDbContext) =
32 | type RpcUserSessionImpl() =
33 | inherit RpcUserSession()
34 |
35 | let canGetLogged login password =
36 | login = "admin" && password = "admin"
37 |
38 | override this.GetLogin() =
39 | WebSharper.Web.Remoting.GetContext().UserSession.GetLoggedInUser()
40 |
41 | override this.Login(login: string) =
42 | //Validate email...
43 | WebSharper.Web.Remoting.GetContext().UserSession.LoginUser(login)
44 |
45 | override this.Logout() =
46 | WebSharper.Web.Remoting.GetContext().UserSession.Logout()
47 |
48 | override this.CheckCredentials(login:string) (password:string) (keepLogged:bool)
49 | : Async> =
50 | async {
51 | if canGetLogged login password then
52 | do! WebSharper.Web.Remoting.GetContext().UserSession.LoginUser(login)
53 | return Result.Ok "Welcome!"
54 | else
55 | return Result.Error "Invalid credentials."
56 | }
57 |
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.FrontEnd/Component.NavigationBar.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd.Components
2 |
3 | open WebSharper
4 | open WebSharper.UI
5 | open WebSharper.UI.Client
6 | open WebSharper.UI.Html
7 | open WebSharper.JQuery
8 | open WebSharper.JavaScript // require by the Remote<'T> type
9 |
10 | open WebSharperTutorial.FrontEnd
11 | open WebSharperTutorial.FrontEnd.Routes
12 |
13 | []
14 | module NavigationBar =
15 |
16 | let private navItem label callback =
17 | li [ attr.``class`` "nav-item" ]
18 | [
19 | a [ attr.``class`` "nav-link"
20 | on.click (fun _ _ -> callback())
21 | ]
22 | [ text label ]
23 | ]
24 |
25 | let private navBar items =
26 | nav
27 | [ attr.``class`` "navbar navbar-expand-lg navbar-light bg-light" ]
28 | [ a [ attr.``class`` "navbar-brand" ] [ text "W#" ]
29 | button
30 | [ attr.``class`` "navbar-toggler"
31 | attr.``type`` "button"
32 | attr.``data-`` "toggle" "collapse"
33 | attr.``data-`` "target" "#navbarSupportedContent"
34 | Attr.Create "aria-controls" "navbarSupportedContent"
35 | Attr.Create "aria-expanded" "false"
36 | Attr.Create "aria-label" "Toggle navigation"
37 | ]
38 | [ span [ attr.``class`` "navbar-toggler-icon" ] []]
39 |
40 | div
41 | [ attr.``class`` "collapse navbar-collapse"
42 | attr.id "navbarSupportedContent"
43 | ]
44 | [ ul [ attr.``class`` "navbar-nav mr-auto" ] items
45 | ]
46 | ]
47 |
48 | let private buildNavbar items =
49 | items
50 | |> List.map (fun (label,callback) -> navItem label callback)
51 | |> navBar
52 |
53 | let private logoff (router:Var) =
54 | async {
55 | do! Remote.Logout ()
56 | router.Value <- Login
57 | }
58 | |> Async.Start
59 |
60 | let Main (router:Var) =
61 | async {
62 | let! loggedUser =
63 | Remote.GetLogin()
64 |
65 | return
66 | match loggedUser with
67 | | None ->
68 | [ "Login",(fun () -> router.Value <- Login)
69 | ]
70 | |> buildNavbar
71 |
72 | | Some _ ->
73 | [ "Listing",(fun () -> router.Value <- Listing)
74 | "Logout",(fun () -> logoff router)
75 | ]
76 | |> buildNavbar
77 | }
78 | |> Doc.Async
79 |
80 |
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.FrontEnd/Main.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open WebSharper
4 | open WebSharper.Sitelets
5 | open WebSharper.UI
6 | open WebSharper.UI.Server
7 |
8 | module Site =
9 | open WebSharper.UI.Html
10 | open WebSharper.UI.Client // required by the Doc.EmbedView
11 | open WebSharperTutorial.FrontEnd.Routes
12 | open WebSharperTutorial.FrontEnd.Pages
13 |
14 | type MainTemplate = Templating.Template<"templates/Main.html">
15 |
16 | let private MainTemplate ctx action (title: string) (body: Doc list) =
17 | Content.Page(
18 | MainTemplate()
19 | .Title(title)
20 | .Body(body)
21 | .Doc()
22 | )
23 |
24 | let HomePage ctx =
25 | MainTemplate ctx EndPoint.Home "Home" [
26 | h1 [] [text "It works!"]
27 | client <@ div [] [ text "Hi there!" ] @>
28 | ]
29 |
30 | let LoginPage ctx endpoint =
31 | let body =
32 | client
33 | <@ let router = Routes.InstallRouter ()
34 |
35 | router.View
36 | |> View.Map (fun endpoint ->
37 | PageLogin.Main router
38 | )
39 | |> Doc.EmbedView
40 | @>
41 | MainTemplate ctx endpoint "Login" [ body ]
42 |
43 | []
44 | let RouteClientPage () =
45 | let router = Routes.InstallRouter ()
46 |
47 | router.View
48 | |> View.Map (fun endpoint ->
49 | match endpoint with
50 | | EndPoint.Home ->
51 | PageHome.Main router
52 |
53 | | EndPoint.Login ->
54 | PageLogin.Main router
55 |
56 | | EndPoint.Listing ->
57 | div [] [ text "Listing Page - implementation pending" ]
58 |
59 | | EndPoint.Form _ ->
60 | div [] [ text "Form Page - implementation pending" ]
61 |
62 | | _ ->
63 | div [] [ text "implementation pending" ]
64 | )
65 | |> Doc.EmbedView
66 |
67 | let LoadClientPage ctx title endpoint =
68 | let body = client <@ RouteClientPage() @>
69 | MainTemplate ctx endpoint title [ body ]
70 |
71 | []
72 | let Main =
73 | Sitelet.New
74 | SiteRouter
75 | (fun ctx endpoint ->
76 | let loggedUser =
77 | async {
78 | return! ctx.UserSession.GetLoggedInUser()
79 | }
80 | |> Async.RunSynchronously
81 |
82 | match loggedUser with
83 | | None -> // user is not authenticated. Allow only public EndPoints
84 | match endpoint with
85 | | EndPoint.Home ->
86 | LoadClientPage ctx "Home" endpoint
87 |
88 | | EndPoint.Login ->
89 | LoadClientPage ctx "Login" endpoint
90 |
91 | | EndPoint.AccessDenied ->
92 | MainTemplate ctx endpoint "Access Denied Page"
93 | [ div [] [ text "Access denied" ] ]
94 | | _ ->
95 | Content.RedirectTemporary AccessDenied
96 |
97 | | Some (u) -> // user is authenticated. Allow all EndPoints
98 | match endpoint with
99 | | EndPoint.Home ->
100 | LoadClientPage ctx "Home" endpoint
101 |
102 | | EndPoint.Login ->
103 | LoadClientPage ctx "Login" endpoint
104 |
105 | | EndPoint.Listing ->
106 | LoadClientPage ctx "Listing Page" endpoint
107 |
108 | | EndPoint.Form code ->
109 | LoadClientPage ctx "Form Page" endpoint
110 |
111 | | _ ->
112 | MainTemplate ctx endpoint "not implemented"
113 | [ div [] [ text "implementation pending" ] ]
114 | )
115 |
116 |
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.FrontEnd/Page.Home.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd.Pages
2 |
3 | open WebSharper
4 | open WebSharper.UI
5 | open WebSharper.UI.Client
6 | open WebSharper.UI.Html
7 | open WebSharper.JQuery
8 | open WebSharper.JavaScript // require by the Remote<'T> type
9 |
10 | open WebSharperTutorial.FrontEnd
11 | open WebSharperTutorial.FrontEnd.Components
12 |
13 | []
14 | module PageHome =
15 |
16 | let Main router =
17 | let navBar =
18 | NavigationBar.Main router
19 |
20 | [
21 | navBar
22 | div [ attr.``class`` "container" ]
23 | [
24 | div [ attr.``class`` "row" ]
25 | [ div [ attr.``class`` "col-xs-12 col-sm-6 mx-auto" ]
26 | [ text "this is the home page" ]
27 | ]
28 | ]
29 | ]
30 | |> Doc.Concat
31 |
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.FrontEnd/Page.Login.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd.Pages
2 |
3 | open WebSharper
4 | open WebSharper.UI
5 | open WebSharper.UI.Client
6 | open WebSharper.UI.Html
7 | open WebSharper.JQuery
8 | open WebSharper.JavaScript // require by the Remote<'T> type
9 |
10 | open WebSharperTutorial.FrontEnd
11 | open WebSharperTutorial.FrontEnd.Components
12 |
13 | []
14 | module PageLogin =
15 |
16 | type private loginFormTemplate = Templating.Template<"templates/Page.Login.html">
17 |
18 | let private AlertBox (rvStatusMsg:Var) =
19 | rvStatusMsg.View
20 | |> View.Map (fun msgO ->
21 | match msgO with
22 | | None ->
23 | Doc.Empty
24 | | Some msg ->
25 | div [ attr.``class`` "alert alert-primary"
26 | Attr.Create "role" "alert"
27 | ]
28 | [ text msg ]
29 | )
30 | |> Doc.EmbedView
31 |
32 | let private FormLogin (router:Var) =
33 | let rvEmail = Var.Create ""
34 | let rvPassword = Var.Create ""
35 | let rvKeepLogged = Var.Create true
36 | let rvStatusMsg = Var.Create None
37 |
38 | let statusMsgBox = AlertBox rvStatusMsg
39 |
40 | loginFormTemplate()
41 | .AlertBox(statusMsgBox)
42 | .Login(rvEmail)
43 | .Password(rvPassword)
44 | .RememberMe(rvKeepLogged)
45 | .OnLogin(fun _ ->
46 | JQuery.Of("form").One("submit", fun elem ev -> ev.PreventDefault()).Ignore
47 | async {
48 | let! response =
49 | Remote.CheckCredentials rvEmail.Value rvPassword.Value rvKeepLogged.Value
50 | match response with
51 | | Result.Ok c ->
52 | rvEmail.Value <- ""
53 | rvPassword.Value <- ""
54 | rvStatusMsg.Value <- None
55 | router.Value <- Routes.Listing
56 |
57 | | Result.Error error ->
58 | rvStatusMsg.Value <- Some error
59 | }
60 | |> Async.Start
61 | )
62 | .OnLogout(fun _ ->
63 | async {
64 | do! Remote.Logout ()
65 | Var.Set router Routes.Home
66 | }
67 | |> Async.Start
68 | )
69 | .Doc()
70 |
71 | let Main router =
72 | let formLogin = FormLogin router
73 | let navBar =
74 | NavigationBar.Main router
75 |
76 | [
77 | navBar
78 | div [ attr.``class`` "container" ]
79 | [
80 | div [ attr.``class`` "row" ]
81 | [ div [ attr.``class`` "col-xs-12 col-sm-6 mx-auto" ] [ formLogin ]
82 | ]
83 | ]
84 | ]
85 | |> Doc.Concat
86 |
87 |
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.FrontEnd/Resources.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 | open WebSharper
5 | open WebSharper.Resources
6 |
7 | module AppResources =
8 |
9 | module Bootstrap =
10 | [)>]
11 | type Js() =
12 | inherit BaseResource("/vendor/bootstrap/js/bootstrap.bundle.min.js")
13 | type Css() =
14 | inherit BaseResource("/vendor/bootstrap/css/bootstrap.min.css")
15 |
16 | module FrontEndApp =
17 | type Css() =
18 | inherit BaseResource("/app/css/common.css")
19 |
20 | type Js() =
21 | inherit BaseResource("/app/js/common.js")
22 |
23 | [);
24 | assembly:Require(typeof);
25 | assembly:Require(typeof);
26 | assembly:Require(typeof);
27 | >]
28 | do()
29 |
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.FrontEnd/Routes.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 | open WebSharper
5 | open WebSharper.Sitelets
6 | open WebSharper.UI
7 |
8 | module Routes =
9 |
10 | []
11 | type EndPoint =
12 | | [] Home
13 | | [] Login
14 | | [] AccessDenied
15 | | [] Listing
16 | | [] Form of int64
17 |
18 | (* Router is used by both client and server side *)
19 | []
20 | let SiteRouter : Router =
21 | let link endPoint =
22 | match endPoint with
23 | | Home -> [ ]
24 | | Login -> [ "login" ]
25 | | AccessDenied -> [ "access-denied" ]
26 | | Listing -> [ "private"; "listing" ]
27 | | Form code -> [ "private"; "form"; string code ]
28 |
29 | let route (path) =
30 | match path with
31 | | [ ] -> Some Home
32 | | [ "login" ] -> Some Login
33 | | [ "access-denied" ] -> Some AccessDenied
34 | | [ "private"; "listing" ] -> Some Listing
35 | | [ "private"; "form"; code ] -> Some (Form (int64 code))
36 | | _ -> None
37 |
38 | Router.Create link route
39 |
40 | []
41 | let InstallRouter () =
42 | let router =
43 | SiteRouter
44 | |> Router.Slice
45 | (fun endpoint ->
46 | (* Turn off client side routing for AccessDenied endpoint *)
47 | match endpoint with
48 | | AccessDenied -> None
49 | | _ -> Some endpoint
50 | )
51 | id
52 | |> Router.Install Home
53 | router
54 |
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.FrontEnd/Startup.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 | open Microsoft.AspNetCore
5 | open Microsoft.AspNetCore.Builder
6 | open Microsoft.AspNetCore.Hosting
7 | open Microsoft.AspNetCore.Http
8 | open Microsoft.Extensions.Configuration
9 | open Microsoft.Extensions.DependencyInjection
10 | open Microsoft.Extensions.Hosting
11 | open WebSharper.AspNetCore
12 |
13 | type Startup() =
14 |
15 | member this.ConfigureServices(services: IServiceCollection) =
16 | services.AddSitelet(Site.Main)
17 | .AddWebSharperRemoting() // <-- add this line
18 | .AddAuthentication("WebSharper")
19 | .AddCookie("WebSharper", fun options -> ())
20 | |> ignore
21 |
22 | member this.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =
23 | if env.IsDevelopment() then app.UseDeveloperExceptionPage() |> ignore
24 |
25 | app.UseAuthentication()
26 | .UseStaticFiles()
27 | .UseWebSharper()
28 | .Run(fun context ->
29 | context.Response.StatusCode <- 404
30 | context.Response.WriteAsync("Page not found"))
31 |
32 | module Program =
33 | let BuildWebHost args =
34 | WebHost
35 | .CreateDefaultBuilder(args)
36 | .UseStartup()
37 | .Build()
38 |
39 | []
40 | let main args =
41 | BuildWebHost(args).Run()
42 | 0
43 |
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.FrontEnd/WebSharperTutorial.FrontEnd.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.FrontEnd/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "websharper": {
3 | "WebSharper.JQuery.Resources.JQuery": "https://code.jquery.com/jquery-3.2.1.min.js"
4 | }
5 | }
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.FrontEnd/templates/Main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | ${Title}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.FrontEnd/templates/Page.Login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Provide your credentials
4 |
5 |
6 |
7 |
8 |
28 |
29 |
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.FrontEnd/wsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://websharper.com/wsconfig.schema.json",
3 | "project": "site",
4 | "outputDir": "wwwroot"
5 | }
6 |
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.FrontEnd/wwwroot/app/css/common.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/code/chapter-04/WebSharperTutorial.FrontEnd/wwwroot/app/css/common.css
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.FrontEnd/wwwroot/app/js/common.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/code/chapter-04/WebSharperTutorial.FrontEnd/wwwroot/app/js/common.js
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.FrontEnd/wwwroot/vendor/bootstrap/css/bootstrap-reboot.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v4.5.0 (https://getbootstrap.com/)
3 | * Copyright 2011-2020 The Bootstrap Authors
4 | * Copyright 2011-2020 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */
8 | *,
9 | *::before,
10 | *::after {
11 | box-sizing: border-box;
12 | }
13 |
14 | html {
15 | font-family: sans-serif;
16 | line-height: 1.15;
17 | -webkit-text-size-adjust: 100%;
18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
19 | }
20 |
21 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
22 | display: block;
23 | }
24 |
25 | body {
26 | margin: 0;
27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
28 | font-size: 1rem;
29 | font-weight: 400;
30 | line-height: 1.5;
31 | color: #212529;
32 | text-align: left;
33 | background-color: #fff;
34 | }
35 |
36 | [tabindex="-1"]:focus:not(:focus-visible) {
37 | outline: 0 !important;
38 | }
39 |
40 | hr {
41 | box-sizing: content-box;
42 | height: 0;
43 | overflow: visible;
44 | }
45 |
46 | h1, h2, h3, h4, h5, h6 {
47 | margin-top: 0;
48 | margin-bottom: 0.5rem;
49 | }
50 |
51 | p {
52 | margin-top: 0;
53 | margin-bottom: 1rem;
54 | }
55 |
56 | abbr[title],
57 | abbr[data-original-title] {
58 | text-decoration: underline;
59 | -webkit-text-decoration: underline dotted;
60 | text-decoration: underline dotted;
61 | cursor: help;
62 | border-bottom: 0;
63 | -webkit-text-decoration-skip-ink: none;
64 | text-decoration-skip-ink: none;
65 | }
66 |
67 | address {
68 | margin-bottom: 1rem;
69 | font-style: normal;
70 | line-height: inherit;
71 | }
72 |
73 | ol,
74 | ul,
75 | dl {
76 | margin-top: 0;
77 | margin-bottom: 1rem;
78 | }
79 |
80 | ol ol,
81 | ul ul,
82 | ol ul,
83 | ul ol {
84 | margin-bottom: 0;
85 | }
86 |
87 | dt {
88 | font-weight: 700;
89 | }
90 |
91 | dd {
92 | margin-bottom: .5rem;
93 | margin-left: 0;
94 | }
95 |
96 | blockquote {
97 | margin: 0 0 1rem;
98 | }
99 |
100 | b,
101 | strong {
102 | font-weight: bolder;
103 | }
104 |
105 | small {
106 | font-size: 80%;
107 | }
108 |
109 | sub,
110 | sup {
111 | position: relative;
112 | font-size: 75%;
113 | line-height: 0;
114 | vertical-align: baseline;
115 | }
116 |
117 | sub {
118 | bottom: -.25em;
119 | }
120 |
121 | sup {
122 | top: -.5em;
123 | }
124 |
125 | a {
126 | color: #007bff;
127 | text-decoration: none;
128 | background-color: transparent;
129 | }
130 |
131 | a:hover {
132 | color: #0056b3;
133 | text-decoration: underline;
134 | }
135 |
136 | a:not([href]) {
137 | color: inherit;
138 | text-decoration: none;
139 | }
140 |
141 | a:not([href]):hover {
142 | color: inherit;
143 | text-decoration: none;
144 | }
145 |
146 | pre,
147 | code,
148 | kbd,
149 | samp {
150 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
151 | font-size: 1em;
152 | }
153 |
154 | pre {
155 | margin-top: 0;
156 | margin-bottom: 1rem;
157 | overflow: auto;
158 | -ms-overflow-style: scrollbar;
159 | }
160 |
161 | figure {
162 | margin: 0 0 1rem;
163 | }
164 |
165 | img {
166 | vertical-align: middle;
167 | border-style: none;
168 | }
169 |
170 | svg {
171 | overflow: hidden;
172 | vertical-align: middle;
173 | }
174 |
175 | table {
176 | border-collapse: collapse;
177 | }
178 |
179 | caption {
180 | padding-top: 0.75rem;
181 | padding-bottom: 0.75rem;
182 | color: #6c757d;
183 | text-align: left;
184 | caption-side: bottom;
185 | }
186 |
187 | th {
188 | text-align: inherit;
189 | }
190 |
191 | label {
192 | display: inline-block;
193 | margin-bottom: 0.5rem;
194 | }
195 |
196 | button {
197 | border-radius: 0;
198 | }
199 |
200 | button:focus {
201 | outline: 1px dotted;
202 | outline: 5px auto -webkit-focus-ring-color;
203 | }
204 |
205 | input,
206 | button,
207 | select,
208 | optgroup,
209 | textarea {
210 | margin: 0;
211 | font-family: inherit;
212 | font-size: inherit;
213 | line-height: inherit;
214 | }
215 |
216 | button,
217 | input {
218 | overflow: visible;
219 | }
220 |
221 | button,
222 | select {
223 | text-transform: none;
224 | }
225 |
226 | [role="button"] {
227 | cursor: pointer;
228 | }
229 |
230 | select {
231 | word-wrap: normal;
232 | }
233 |
234 | button,
235 | [type="button"],
236 | [type="reset"],
237 | [type="submit"] {
238 | -webkit-appearance: button;
239 | }
240 |
241 | button:not(:disabled),
242 | [type="button"]:not(:disabled),
243 | [type="reset"]:not(:disabled),
244 | [type="submit"]:not(:disabled) {
245 | cursor: pointer;
246 | }
247 |
248 | button::-moz-focus-inner,
249 | [type="button"]::-moz-focus-inner,
250 | [type="reset"]::-moz-focus-inner,
251 | [type="submit"]::-moz-focus-inner {
252 | padding: 0;
253 | border-style: none;
254 | }
255 |
256 | input[type="radio"],
257 | input[type="checkbox"] {
258 | box-sizing: border-box;
259 | padding: 0;
260 | }
261 |
262 | textarea {
263 | overflow: auto;
264 | resize: vertical;
265 | }
266 |
267 | fieldset {
268 | min-width: 0;
269 | padding: 0;
270 | margin: 0;
271 | border: 0;
272 | }
273 |
274 | legend {
275 | display: block;
276 | width: 100%;
277 | max-width: 100%;
278 | padding: 0;
279 | margin-bottom: .5rem;
280 | font-size: 1.5rem;
281 | line-height: inherit;
282 | color: inherit;
283 | white-space: normal;
284 | }
285 |
286 | progress {
287 | vertical-align: baseline;
288 | }
289 |
290 | [type="number"]::-webkit-inner-spin-button,
291 | [type="number"]::-webkit-outer-spin-button {
292 | height: auto;
293 | }
294 |
295 | [type="search"] {
296 | outline-offset: -2px;
297 | -webkit-appearance: none;
298 | }
299 |
300 | [type="search"]::-webkit-search-decoration {
301 | -webkit-appearance: none;
302 | }
303 |
304 | ::-webkit-file-upload-button {
305 | font: inherit;
306 | -webkit-appearance: button;
307 | }
308 |
309 | output {
310 | display: inline-block;
311 | }
312 |
313 | summary {
314 | display: list-item;
315 | cursor: pointer;
316 | }
317 |
318 | template {
319 | display: none;
320 | }
321 |
322 | [hidden] {
323 | display: none !important;
324 | }
325 | /*# sourceMappingURL=bootstrap-reboot.css.map */
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.FrontEnd/wwwroot/vendor/bootstrap/css/bootstrap-reboot.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v4.5.0 (https://getbootstrap.com/)
3 | * Copyright 2011-2020 The Bootstrap Authors
4 | * Copyright 2011-2020 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */
--------------------------------------------------------------------------------
/code/chapter-04/WebSharperTutorial.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26124.0
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WebSharperTutorial.FrontEnd", "WebSharperTutorial.FrontEnd\WebSharperTutorial.FrontEnd.fsproj", "{6870CF92-C520-4FC9-8814-C93AECA5F5D6}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Debug|x64 = Debug|x64
12 | Debug|x86 = Debug|x86
13 | Release|Any CPU = Release|Any CPU
14 | Release|x64 = Release|x64
15 | Release|x86 = Release|x86
16 | EndGlobalSection
17 | GlobalSection(SolutionProperties) = preSolution
18 | HideSolutionNode = FALSE
19 | EndGlobalSection
20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
21 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
22 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
23 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x64.ActiveCfg = Debug|Any CPU
24 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x64.Build.0 = Debug|Any CPU
25 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x86.ActiveCfg = Debug|Any CPU
26 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x86.Build.0 = Debug|Any CPU
27 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|Any CPU.Build.0 = Release|Any CPU
29 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x64.ActiveCfg = Release|Any CPU
30 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x64.Build.0 = Release|Any CPU
31 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x86.ActiveCfg = Release|Any CPU
32 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x86.Build.0 = Release|Any CPU
33 | EndGlobalSection
34 | EndGlobal
35 |
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/Auth.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | module Auth =
4 | open System
5 |
6 | open WebSharper
7 | open WebSharper.Web
8 | open WebSharper.AspNetCore
9 | open Microsoft.AspNetCore.Identity
10 |
11 | let GetLoggedInUser () =
12 | let ctx = Remoting.GetContext()
13 | ctx.UserSession.GetLoggedInUser()
14 |
15 | []
16 | type RpcUserSession() =
17 | []
18 | abstract GetLogin : unit -> Async>
19 | []
20 | abstract Login : login: string -> Async
21 | []
22 | abstract Logout : unit -> Async
23 | []
24 | abstract CheckCredentials : string -> string -> bool -> Async>
25 |
26 |
27 | []
28 | type ApplicationUser() =
29 | inherit IdentityUser()
30 |
31 | //type RpcUserSessionImpl(dbContext: Database.AppDbContext) =
32 | type RpcUserSessionImpl() =
33 | inherit RpcUserSession()
34 |
35 | let canGetLogged login password =
36 | login = "admin" && password = "admin"
37 |
38 | override this.GetLogin() =
39 | WebSharper.Web.Remoting.GetContext().UserSession.GetLoggedInUser()
40 |
41 | override this.Login(login: string) =
42 | //Validate email...
43 | WebSharper.Web.Remoting.GetContext().UserSession.LoginUser(login)
44 |
45 | override this.Logout() =
46 | WebSharper.Web.Remoting.GetContext().UserSession.Logout()
47 |
48 | override this.CheckCredentials(login:string) (password:string) (keepLogged:bool)
49 | : Async> =
50 | async {
51 | if canGetLogged login password then
52 | do! WebSharper.Web.Remoting.GetContext().UserSession.LoginUser(login)
53 | return Result.Ok "Welcome!"
54 | else
55 | return Result.Error "Invalid credentials."
56 | }
57 |
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/Component.NavigationBar.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd.Components
2 |
3 | open WebSharper
4 | open WebSharper.UI
5 | open WebSharper.UI.Client
6 | open WebSharper.UI.Html
7 | open WebSharper.JQuery
8 | open WebSharper.JavaScript // require by the Remote<'T> type
9 |
10 | open WebSharperTutorial.FrontEnd
11 | open WebSharperTutorial.FrontEnd.Routes
12 |
13 | []
14 | module NavigationBar =
15 |
16 | let private navItem label callback =
17 | li [ attr.``class`` "nav-item" ]
18 | [
19 | a [ attr.``class`` "nav-link"
20 | on.click (fun _ _ -> callback())
21 | ]
22 | [ text label ]
23 | ]
24 |
25 | let private navBar items =
26 | nav
27 | [ attr.``class`` "navbar navbar-expand-lg navbar-light bg-light" ]
28 | [ a [ attr.``class`` "navbar-brand" ] [ text "W#" ]
29 | button
30 | [ attr.``class`` "navbar-toggler"
31 | attr.``type`` "button"
32 | attr.``data-`` "toggle" "collapse"
33 | attr.``data-`` "target" "#navbarSupportedContent"
34 | Attr.Create "aria-controls" "navbarSupportedContent"
35 | Attr.Create "aria-expanded" "false"
36 | Attr.Create "aria-label" "Toggle navigation"
37 | ]
38 | [ span [ attr.``class`` "navbar-toggler-icon" ] []]
39 |
40 | div
41 | [ attr.``class`` "collapse navbar-collapse"
42 | attr.id "navbarSupportedContent"
43 | ]
44 | [ ul [ attr.``class`` "navbar-nav mr-auto" ] items
45 | ]
46 | ]
47 |
48 | let private buildNavbar items =
49 | items
50 | |> List.map (fun (label,callback) -> navItem label callback)
51 | |> navBar
52 |
53 | let private logoff (router:Var) =
54 | async {
55 | do! Remote.Logout ()
56 | router.Value <- Login
57 | }
58 | |> Async.Start
59 |
60 | let Main (router:Var) =
61 | async {
62 | let! loggedUser =
63 | Remote.GetLogin()
64 |
65 | return
66 | match loggedUser with
67 | | None ->
68 | [ "Login",(fun () -> router.Value <- Login)
69 | ]
70 | |> buildNavbar
71 |
72 | | Some _ ->
73 | [ "Listing",(fun () -> router.Value <- Listing)
74 | "Logout",(fun () -> logoff router)
75 | ]
76 | |> buildNavbar
77 | }
78 | |> Doc.Async
79 |
80 |
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/DTO.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 |
5 | open WebSharper
6 |
7 | []
8 | module DTO =
9 |
10 | type User = {
11 | Code: int64
12 | Firstname: string
13 | Lastname: string
14 | UpdateDate: DateTime
15 | }
16 |
17 | let CreateUser code firstname lastname updateDate =
18 | {
19 | Code = code
20 | Firstname = firstname
21 | Lastname = lastname
22 | UpdateDate = updateDate
23 | }
24 |
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/Main.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open WebSharper
4 | open WebSharper.Sitelets
5 | open WebSharper.UI
6 | open WebSharper.UI.Server
7 |
8 | module Site =
9 | open WebSharper.UI.Html
10 | open WebSharper.UI.Client // required by the Doc.EmbedView
11 | open WebSharperTutorial.FrontEnd.Routes
12 | open WebSharperTutorial.FrontEnd.Pages
13 |
14 | type MainTemplate = Templating.Template<"templates/Main.html">
15 |
16 | let private MainTemplate ctx action (title: string) (body: Doc list) =
17 | Content.Page(
18 | MainTemplate()
19 | .Title(title)
20 | .Body(body)
21 | .Doc()
22 | )
23 |
24 | []
25 | let RouteClientPage () =
26 | let router = Routes.InstallRouter ()
27 |
28 | router.View
29 | |> View.Map (fun endpoint ->
30 | match endpoint with
31 | | EndPoint.Home ->
32 | PageHome.Main router
33 |
34 | | EndPoint.Login ->
35 | PageLogin.Main router
36 |
37 | | EndPoint.Listing ->
38 | PageListing.Main router
39 |
40 | | EndPoint.Form _ ->
41 | div [] [ text "Form Page - implementation pending" ]
42 |
43 | | _ ->
44 | div [] [ text "implementation pending" ]
45 | )
46 | |> Doc.EmbedView
47 |
48 | let LoadClientPage ctx title endpoint =
49 | let body = client <@ RouteClientPage() @>
50 | MainTemplate ctx endpoint title [ body ]
51 |
52 | []
53 | let Main =
54 | Sitelet.New
55 | SiteRouter
56 | (fun ctx endpoint ->
57 | let loggedUser =
58 | async {
59 | return! ctx.UserSession.GetLoggedInUser()
60 | }
61 | |> Async.RunSynchronously
62 |
63 | match loggedUser with
64 | | None -> // user is not authenticated. Allow only public EndPoints
65 | match endpoint with
66 | | EndPoint.Home ->
67 | LoadClientPage ctx "Home" endpoint
68 |
69 | | EndPoint.Login ->
70 | LoadClientPage ctx "Login" endpoint
71 |
72 | | EndPoint.AccessDenied ->
73 | MainTemplate ctx endpoint "Access Denied Page"
74 | [ div [] [ text "Access denied" ] ]
75 | | _ ->
76 | Content.RedirectTemporary AccessDenied
77 |
78 | | Some (u) -> // user is authenticated. Allow all EndPoints
79 | match endpoint with
80 | | EndPoint.Home ->
81 | LoadClientPage ctx "Home" endpoint
82 |
83 | | EndPoint.Login ->
84 | LoadClientPage ctx "Login" endpoint
85 |
86 | | EndPoint.Listing ->
87 | LoadClientPage ctx "Listing Page" endpoint
88 |
89 | | EndPoint.Form code ->
90 | LoadClientPage ctx "Form Page" endpoint
91 |
92 | | _ ->
93 | MainTemplate ctx endpoint "not implemented"
94 | [ div [] [ text "implementation pending" ] ]
95 | )
96 |
97 |
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/Page.Home.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd.Pages
2 |
3 | open WebSharper
4 | open WebSharper.UI
5 | open WebSharper.UI.Client
6 | open WebSharper.UI.Html
7 | open WebSharper.JQuery
8 | open WebSharper.JavaScript // require by the Remote<'T> type
9 |
10 | open WebSharperTutorial.FrontEnd
11 | open WebSharperTutorial.FrontEnd.Components
12 |
13 | []
14 | module PageHome =
15 |
16 | let Main router =
17 | let navBar =
18 | NavigationBar.Main router
19 |
20 | [
21 | navBar
22 | div [ attr.``class`` "container" ]
23 | [
24 | div [ attr.``class`` "row" ]
25 | [ div [ attr.``class`` "col-xs-12 col-sm-6 mx-auto" ]
26 | [ text "this is the home page" ]
27 | ]
28 | ]
29 | ]
30 | |> Doc.Concat
31 |
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/Page.Listing.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd.Pages
2 |
3 | open WebSharper
4 | open WebSharper.UI
5 | open WebSharper.UI.Client
6 | open WebSharper.UI.Html
7 |
8 | open WebSharperTutorial.FrontEnd
9 | open WebSharperTutorial.FrontEnd.Components
10 |
11 | []
12 | module PageListing =
13 |
14 | type private listingTemplate = Templating.Template<"templates/Page.Listing.html">
15 |
16 | let private buildTable router (users:DTO.User list) =
17 | let tableRows =
18 | users
19 | |> List.map(fun user ->
20 | listingTemplate.RowTemplate()
21 | .Code(string user.Code)
22 | .Firstname(user.Firstname)
23 | .Lastname(user.Lastname)
24 | .UpdatedAt(user.UpdateDate.ToShortDateString())
25 | .OnEdit(fun _ -> Var.Set router (Routes.Form user.Code))
26 | .Doc()
27 | )
28 |
29 | listingTemplate()
30 | .Rows(tableRows)
31 | .Doc()
32 |
33 | let Main router =
34 | async {
35 | let navBar =
36 | NavigationBar.Main router
37 |
38 | let! users =
39 | Server.GetUsers()
40 |
41 | let tableElement =
42 | buildTable router users
43 |
44 | return
45 | [
46 | navBar
47 | div [ attr.``class`` "container" ]
48 | [
49 | div [ attr.``class`` "row" ]
50 | [ div [ attr.``class`` "col-12" ]
51 | [ tableElement ]
52 | ]
53 | ]
54 | ]
55 | |> Doc.Concat
56 | }
57 | |> Doc.Async
58 |
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/Page.Login.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd.Pages
2 |
3 | open WebSharper
4 | open WebSharper.UI
5 | open WebSharper.UI.Client
6 | open WebSharper.UI.Html
7 | open WebSharper.JQuery
8 | open WebSharper.JavaScript // require by the Remote<'T> type
9 |
10 | open WebSharperTutorial.FrontEnd
11 | open WebSharperTutorial.FrontEnd.Components
12 |
13 | []
14 | module PageLogin =
15 |
16 | type private loginFormTemplate = Templating.Template<"templates/Page.Login.html">
17 |
18 | let private AlertBox (rvStatusMsg:Var) =
19 | rvStatusMsg.View
20 | |> View.Map (fun msgO ->
21 | match msgO with
22 | | None ->
23 | Doc.Empty
24 | | Some msg ->
25 | div [ attr.``class`` "alert alert-primary"
26 | Attr.Create "role" "alert"
27 | ]
28 | [ text msg ]
29 | )
30 | |> Doc.EmbedView
31 |
32 | let private FormLogin (router:Var) =
33 | let rvEmail = Var.Create ""
34 | let rvPassword = Var.Create ""
35 | let rvKeepLogged = Var.Create true
36 | let rvStatusMsg = Var.Create None
37 |
38 | let statusMsgBox = AlertBox rvStatusMsg
39 |
40 | loginFormTemplate()
41 | .AlertBox(statusMsgBox)
42 | .Login(rvEmail)
43 | .Password(rvPassword)
44 | .RememberMe(rvKeepLogged)
45 | .OnLogin(fun _ ->
46 | JQuery.Of("form").One("submit", fun elem ev -> ev.PreventDefault()).Ignore
47 | async {
48 | let! response =
49 | Remote.CheckCredentials rvEmail.Value rvPassword.Value rvKeepLogged.Value
50 | match response with
51 | | Result.Ok c ->
52 | rvEmail.Value <- ""
53 | rvPassword.Value <- ""
54 | rvStatusMsg.Value <- None
55 | router.Value <- Routes.Listing
56 |
57 | | Result.Error error ->
58 | rvStatusMsg.Value <- Some error
59 | }
60 | |> Async.Start
61 | )
62 | .OnLogout(fun _ ->
63 | async {
64 | do! Remote.Logout ()
65 | Var.Set router Routes.Home
66 | }
67 | |> Async.Start
68 | )
69 | .Doc()
70 |
71 | let Main router =
72 | let formLogin = FormLogin router
73 | let navBar =
74 | NavigationBar.Main router
75 |
76 | [
77 | navBar
78 | div [ attr.``class`` "container" ]
79 | [
80 | div [ attr.``class`` "row" ]
81 | [ div [ attr.``class`` "col-xs-12 col-sm-6 mx-auto" ] [ formLogin ]
82 | ]
83 | ]
84 | ]
85 | |> Doc.Concat
86 |
87 |
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/Resources.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 | open WebSharper
5 | open WebSharper.Resources
6 |
7 | module AppResources =
8 |
9 | module Bootstrap =
10 | [)>]
11 | type Js() =
12 | inherit BaseResource("/vendor/bootstrap/js/bootstrap.bundle.min.js")
13 | type Css() =
14 | inherit BaseResource("/vendor/bootstrap/css/bootstrap.min.css")
15 |
16 | module FrontEndApp =
17 | type Css() =
18 | inherit BaseResource("/app/css/common.css")
19 |
20 | type Js() =
21 | inherit BaseResource("/app/js/common.js")
22 |
23 | [);
24 | assembly:Require(typeof);
25 | assembly:Require(typeof);
26 | assembly:Require(typeof);
27 | >]
28 | do()
29 |
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/Routes.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 | open WebSharper
5 | open WebSharper.Sitelets
6 | open WebSharper.UI
7 |
8 | module Routes =
9 |
10 | []
11 | type EndPoint =
12 | | [] Home
13 | | [] Login
14 | | [] AccessDenied
15 | | [] Listing
16 | | [] Form of int64
17 |
18 | (* Router is used by both client and server side *)
19 | []
20 | let SiteRouter : Router =
21 | let link endPoint =
22 | match endPoint with
23 | | Home -> [ ]
24 | | Login -> [ "login" ]
25 | | AccessDenied -> [ "access-denied" ]
26 | | Listing -> [ "private"; "listing" ]
27 | | Form code -> [ "private"; "form"; string code ]
28 |
29 | let route (path) =
30 | match path with
31 | | [ ] -> Some Home
32 | | [ "login" ] -> Some Login
33 | | [ "access-denied" ] -> Some AccessDenied
34 | | [ "private"; "listing" ] -> Some Listing
35 | | [ "private"; "form"; code ] -> Some (Form (int64 code))
36 | | _ -> None
37 |
38 | Router.Create link route
39 |
40 | []
41 | let InstallRouter () =
42 | let router =
43 | SiteRouter
44 | |> Router.Slice
45 | (fun endpoint ->
46 | (* Turn off client side routing for AccessDenied endpoint *)
47 | match endpoint with
48 | | AccessDenied -> None
49 | | _ -> Some endpoint
50 | )
51 | id
52 | |> Router.Install Home
53 | router
54 |
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/Server.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 |
5 | open WebSharper
6 |
7 | open WebSharperTutorial.FrontEnd
8 | open WebSharperTutorial.FrontEnd.DTO
9 |
10 | module Server =
11 |
12 | let private dbUsers () =
13 | [
14 | CreateUser 1L "Firstname 1" "Lastname 1" (new DateTime(2020,3,17))
15 | CreateUser 2L "Firstname 2" "Lastname 2" (new DateTime(2019,6,21))
16 | CreateUser 3L "Firstname 3" "Lastname 3" (new DateTime(2019,8,14))
17 | ]
18 |
19 | []
20 | let GetUsers () : Async =
21 | async {
22 | return dbUsers()
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/Startup.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 | open Microsoft.AspNetCore
5 | open Microsoft.AspNetCore.Builder
6 | open Microsoft.AspNetCore.Hosting
7 | open Microsoft.AspNetCore.Http
8 | open Microsoft.Extensions.Configuration
9 | open Microsoft.Extensions.DependencyInjection
10 | open Microsoft.Extensions.Hosting
11 | open WebSharper.AspNetCore
12 |
13 | type Startup() =
14 |
15 | member this.ConfigureServices(services: IServiceCollection) =
16 | services.AddSitelet(Site.Main)
17 | .AddWebSharperRemoting() // <-- add this line
18 | .AddAuthentication("WebSharper")
19 | .AddCookie("WebSharper", fun options -> ())
20 | |> ignore
21 |
22 | member this.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =
23 | if env.IsDevelopment() then app.UseDeveloperExceptionPage() |> ignore
24 |
25 | app.UseAuthentication()
26 | .UseStaticFiles()
27 | .UseWebSharper()
28 | .Run(fun context ->
29 | context.Response.StatusCode <- 404
30 | context.Response.WriteAsync("Page not found"))
31 |
32 | module Program =
33 | let BuildWebHost args =
34 | WebHost
35 | .CreateDefaultBuilder(args)
36 | .UseStartup()
37 | .Build()
38 |
39 | []
40 | let main args =
41 | BuildWebHost(args).Run()
42 | 0
43 |
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/WebSharperTutorial.FrontEnd.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "websharper": {
3 | "WebSharper.JQuery.Resources.JQuery": "https://code.jquery.com/jquery-3.2.1.min.js"
4 | }
5 | }
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/templates/Main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | ${Title}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/templates/Page.Listing.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #
5 | First
6 | Last
7 | Updated At
8 |
9 |
10 |
11 |
12 | ${Code}
13 | ${Firstname}
14 | ${Lastname}
15 | ${UpdatedAt}
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/templates/Page.Login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Provide your credentials
4 |
5 |
6 |
7 |
8 |
28 |
29 |
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/wsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://websharper.com/wsconfig.schema.json",
3 | "project": "site",
4 | "outputDir": "wwwroot"
5 | }
6 |
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/wwwroot/app/css/common.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/code/chapter-05/WebSharperTutorial.FrontEnd/wwwroot/app/css/common.css
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/wwwroot/app/js/common.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/code/chapter-05/WebSharperTutorial.FrontEnd/wwwroot/app/js/common.js
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/wwwroot/vendor/bootstrap/css/bootstrap-reboot.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v4.5.0 (https://getbootstrap.com/)
3 | * Copyright 2011-2020 The Bootstrap Authors
4 | * Copyright 2011-2020 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */
8 | *,
9 | *::before,
10 | *::after {
11 | box-sizing: border-box;
12 | }
13 |
14 | html {
15 | font-family: sans-serif;
16 | line-height: 1.15;
17 | -webkit-text-size-adjust: 100%;
18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
19 | }
20 |
21 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
22 | display: block;
23 | }
24 |
25 | body {
26 | margin: 0;
27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
28 | font-size: 1rem;
29 | font-weight: 400;
30 | line-height: 1.5;
31 | color: #212529;
32 | text-align: left;
33 | background-color: #fff;
34 | }
35 |
36 | [tabindex="-1"]:focus:not(:focus-visible) {
37 | outline: 0 !important;
38 | }
39 |
40 | hr {
41 | box-sizing: content-box;
42 | height: 0;
43 | overflow: visible;
44 | }
45 |
46 | h1, h2, h3, h4, h5, h6 {
47 | margin-top: 0;
48 | margin-bottom: 0.5rem;
49 | }
50 |
51 | p {
52 | margin-top: 0;
53 | margin-bottom: 1rem;
54 | }
55 |
56 | abbr[title],
57 | abbr[data-original-title] {
58 | text-decoration: underline;
59 | -webkit-text-decoration: underline dotted;
60 | text-decoration: underline dotted;
61 | cursor: help;
62 | border-bottom: 0;
63 | -webkit-text-decoration-skip-ink: none;
64 | text-decoration-skip-ink: none;
65 | }
66 |
67 | address {
68 | margin-bottom: 1rem;
69 | font-style: normal;
70 | line-height: inherit;
71 | }
72 |
73 | ol,
74 | ul,
75 | dl {
76 | margin-top: 0;
77 | margin-bottom: 1rem;
78 | }
79 |
80 | ol ol,
81 | ul ul,
82 | ol ul,
83 | ul ol {
84 | margin-bottom: 0;
85 | }
86 |
87 | dt {
88 | font-weight: 700;
89 | }
90 |
91 | dd {
92 | margin-bottom: .5rem;
93 | margin-left: 0;
94 | }
95 |
96 | blockquote {
97 | margin: 0 0 1rem;
98 | }
99 |
100 | b,
101 | strong {
102 | font-weight: bolder;
103 | }
104 |
105 | small {
106 | font-size: 80%;
107 | }
108 |
109 | sub,
110 | sup {
111 | position: relative;
112 | font-size: 75%;
113 | line-height: 0;
114 | vertical-align: baseline;
115 | }
116 |
117 | sub {
118 | bottom: -.25em;
119 | }
120 |
121 | sup {
122 | top: -.5em;
123 | }
124 |
125 | a {
126 | color: #007bff;
127 | text-decoration: none;
128 | background-color: transparent;
129 | }
130 |
131 | a:hover {
132 | color: #0056b3;
133 | text-decoration: underline;
134 | }
135 |
136 | a:not([href]) {
137 | color: inherit;
138 | text-decoration: none;
139 | }
140 |
141 | a:not([href]):hover {
142 | color: inherit;
143 | text-decoration: none;
144 | }
145 |
146 | pre,
147 | code,
148 | kbd,
149 | samp {
150 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
151 | font-size: 1em;
152 | }
153 |
154 | pre {
155 | margin-top: 0;
156 | margin-bottom: 1rem;
157 | overflow: auto;
158 | -ms-overflow-style: scrollbar;
159 | }
160 |
161 | figure {
162 | margin: 0 0 1rem;
163 | }
164 |
165 | img {
166 | vertical-align: middle;
167 | border-style: none;
168 | }
169 |
170 | svg {
171 | overflow: hidden;
172 | vertical-align: middle;
173 | }
174 |
175 | table {
176 | border-collapse: collapse;
177 | }
178 |
179 | caption {
180 | padding-top: 0.75rem;
181 | padding-bottom: 0.75rem;
182 | color: #6c757d;
183 | text-align: left;
184 | caption-side: bottom;
185 | }
186 |
187 | th {
188 | text-align: inherit;
189 | }
190 |
191 | label {
192 | display: inline-block;
193 | margin-bottom: 0.5rem;
194 | }
195 |
196 | button {
197 | border-radius: 0;
198 | }
199 |
200 | button:focus {
201 | outline: 1px dotted;
202 | outline: 5px auto -webkit-focus-ring-color;
203 | }
204 |
205 | input,
206 | button,
207 | select,
208 | optgroup,
209 | textarea {
210 | margin: 0;
211 | font-family: inherit;
212 | font-size: inherit;
213 | line-height: inherit;
214 | }
215 |
216 | button,
217 | input {
218 | overflow: visible;
219 | }
220 |
221 | button,
222 | select {
223 | text-transform: none;
224 | }
225 |
226 | [role="button"] {
227 | cursor: pointer;
228 | }
229 |
230 | select {
231 | word-wrap: normal;
232 | }
233 |
234 | button,
235 | [type="button"],
236 | [type="reset"],
237 | [type="submit"] {
238 | -webkit-appearance: button;
239 | }
240 |
241 | button:not(:disabled),
242 | [type="button"]:not(:disabled),
243 | [type="reset"]:not(:disabled),
244 | [type="submit"]:not(:disabled) {
245 | cursor: pointer;
246 | }
247 |
248 | button::-moz-focus-inner,
249 | [type="button"]::-moz-focus-inner,
250 | [type="reset"]::-moz-focus-inner,
251 | [type="submit"]::-moz-focus-inner {
252 | padding: 0;
253 | border-style: none;
254 | }
255 |
256 | input[type="radio"],
257 | input[type="checkbox"] {
258 | box-sizing: border-box;
259 | padding: 0;
260 | }
261 |
262 | textarea {
263 | overflow: auto;
264 | resize: vertical;
265 | }
266 |
267 | fieldset {
268 | min-width: 0;
269 | padding: 0;
270 | margin: 0;
271 | border: 0;
272 | }
273 |
274 | legend {
275 | display: block;
276 | width: 100%;
277 | max-width: 100%;
278 | padding: 0;
279 | margin-bottom: .5rem;
280 | font-size: 1.5rem;
281 | line-height: inherit;
282 | color: inherit;
283 | white-space: normal;
284 | }
285 |
286 | progress {
287 | vertical-align: baseline;
288 | }
289 |
290 | [type="number"]::-webkit-inner-spin-button,
291 | [type="number"]::-webkit-outer-spin-button {
292 | height: auto;
293 | }
294 |
295 | [type="search"] {
296 | outline-offset: -2px;
297 | -webkit-appearance: none;
298 | }
299 |
300 | [type="search"]::-webkit-search-decoration {
301 | -webkit-appearance: none;
302 | }
303 |
304 | ::-webkit-file-upload-button {
305 | font: inherit;
306 | -webkit-appearance: button;
307 | }
308 |
309 | output {
310 | display: inline-block;
311 | }
312 |
313 | summary {
314 | display: list-item;
315 | cursor: pointer;
316 | }
317 |
318 | template {
319 | display: none;
320 | }
321 |
322 | [hidden] {
323 | display: none !important;
324 | }
325 | /*# sourceMappingURL=bootstrap-reboot.css.map */
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.FrontEnd/wwwroot/vendor/bootstrap/css/bootstrap-reboot.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v4.5.0 (https://getbootstrap.com/)
3 | * Copyright 2011-2020 The Bootstrap Authors
4 | * Copyright 2011-2020 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */
--------------------------------------------------------------------------------
/code/chapter-05/WebSharperTutorial.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26124.0
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WebSharperTutorial.FrontEnd", "WebSharperTutorial.FrontEnd\WebSharperTutorial.FrontEnd.fsproj", "{6870CF92-C520-4FC9-8814-C93AECA5F5D6}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Debug|x64 = Debug|x64
12 | Debug|x86 = Debug|x86
13 | Release|Any CPU = Release|Any CPU
14 | Release|x64 = Release|x64
15 | Release|x86 = Release|x86
16 | EndGlobalSection
17 | GlobalSection(SolutionProperties) = preSolution
18 | HideSolutionNode = FALSE
19 | EndGlobalSection
20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
21 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
22 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
23 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x64.ActiveCfg = Debug|Any CPU
24 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x64.Build.0 = Debug|Any CPU
25 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x86.ActiveCfg = Debug|Any CPU
26 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x86.Build.0 = Debug|Any CPU
27 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|Any CPU.Build.0 = Release|Any CPU
29 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x64.ActiveCfg = Release|Any CPU
30 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x64.Build.0 = Release|Any CPU
31 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x86.ActiveCfg = Release|Any CPU
32 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x86.Build.0 = Release|Any CPU
33 | EndGlobalSection
34 | EndGlobal
35 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/Auth.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | module Auth =
4 | open System
5 |
6 | open WebSharper
7 | open WebSharper.Web
8 | open WebSharper.AspNetCore
9 | open Microsoft.AspNetCore.Identity
10 |
11 | let GetLoggedInUser () =
12 | let ctx = Remoting.GetContext()
13 | ctx.UserSession.GetLoggedInUser()
14 |
15 | []
16 | type RpcUserSession() =
17 | []
18 | abstract GetLogin : unit -> Async>
19 | []
20 | abstract Login : login: string -> Async
21 | []
22 | abstract Logout : unit -> Async
23 | []
24 | abstract CheckCredentials : string -> string -> bool -> Async>
25 |
26 |
27 | []
28 | type ApplicationUser() =
29 | inherit IdentityUser()
30 |
31 | //type RpcUserSessionImpl(dbContext: Database.AppDbContext) =
32 | type RpcUserSessionImpl() =
33 | inherit RpcUserSession()
34 |
35 | let canGetLogged login password =
36 | login = "admin" && password = "admin"
37 |
38 | override this.GetLogin() =
39 | WebSharper.Web.Remoting.GetContext().UserSession.GetLoggedInUser()
40 |
41 | override this.Login(login: string) =
42 | //Validate email...
43 | WebSharper.Web.Remoting.GetContext().UserSession.LoginUser(login)
44 |
45 | override this.Logout() =
46 | WebSharper.Web.Remoting.GetContext().UserSession.Logout()
47 |
48 | override this.CheckCredentials(login:string) (password:string) (keepLogged:bool)
49 | : Async> =
50 | async {
51 | if canGetLogged login password then
52 | do! WebSharper.Web.Remoting.GetContext().UserSession.LoginUser(login)
53 | return Result.Ok "Welcome!"
54 | else
55 | return Result.Error "Invalid credentials."
56 | }
57 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/Component.NavigationBar.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd.Components
2 |
3 | open WebSharper
4 | open WebSharper.UI
5 | open WebSharper.UI.Client
6 | open WebSharper.UI.Html
7 | open WebSharper.JQuery
8 | open WebSharper.JavaScript // require by the Remote<'T> type
9 |
10 | open WebSharperTutorial.FrontEnd
11 | open WebSharperTutorial.FrontEnd.Routes
12 |
13 | []
14 | module NavigationBar =
15 |
16 | let private navItem label callback =
17 | li [ attr.``class`` "nav-item" ]
18 | [
19 | a [ attr.``class`` "nav-link"
20 | on.click (fun _ _ -> callback())
21 | ]
22 | [ text label ]
23 | ]
24 |
25 | let private navBar items =
26 | nav
27 | [ attr.``class`` "navbar navbar-expand-lg navbar-light bg-light" ]
28 | [ a [ attr.``class`` "navbar-brand" ] [ text "W#" ]
29 | button
30 | [ attr.``class`` "navbar-toggler"
31 | attr.``type`` "button"
32 | attr.``data-`` "toggle" "collapse"
33 | attr.``data-`` "target" "#navbarSupportedContent"
34 | Attr.Create "aria-controls" "navbarSupportedContent"
35 | Attr.Create "aria-expanded" "false"
36 | Attr.Create "aria-label" "Toggle navigation"
37 | ]
38 | [ span [ attr.``class`` "navbar-toggler-icon" ] []]
39 |
40 | div
41 | [ attr.``class`` "collapse navbar-collapse"
42 | attr.id "navbarSupportedContent"
43 | ]
44 | [ ul [ attr.``class`` "navbar-nav mr-auto" ] items
45 | ]
46 | ]
47 |
48 | let private buildNavbar items =
49 | items
50 | |> List.map (fun (label,callback) -> navItem label callback)
51 | |> navBar
52 |
53 | let private logoff (router:Var) =
54 | async {
55 | do! Remote.Logout ()
56 | router.Value <- Login
57 | }
58 | |> Async.Start
59 |
60 | let Main (router:Var) =
61 | async {
62 | let! loggedUser =
63 | Remote.GetLogin()
64 |
65 | return
66 | match loggedUser with
67 | | None ->
68 | [ "Login",(fun () -> router.Value <- Login)
69 | ]
70 | |> buildNavbar
71 |
72 | | Some _ ->
73 | [ "Listing",(fun () -> router.Value <- Listing)
74 | "Logout",(fun () -> logoff router)
75 | ]
76 | |> buildNavbar
77 | }
78 | |> Doc.Async
79 |
80 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/DTO.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 |
5 | open WebSharper
6 |
7 | []
8 | module DTO =
9 |
10 | type User = {
11 | Code: int64
12 | Firstname: string
13 | Lastname: string
14 | UpdateDate: DateTime
15 | }
16 |
17 | let CreateUser code firstname lastname updateDate =
18 | {
19 | Code = code
20 | Firstname = firstname
21 | Lastname = lastname
22 | UpdateDate = updateDate
23 | }
24 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/Main.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open WebSharper
4 | open WebSharper.Sitelets
5 | open WebSharper.UI
6 | open WebSharper.UI.Server
7 |
8 | module Site =
9 | open WebSharper.UI.Html
10 | open WebSharper.UI.Client // required by the Doc.EmbedView
11 | open WebSharperTutorial.FrontEnd.Routes
12 | open WebSharperTutorial.FrontEnd.Pages
13 |
14 | type MainTemplate = Templating.Template<"templates/Main.html">
15 |
16 | let private MainTemplate ctx action (title: string) (body: Doc list) =
17 | Content.Page(
18 | MainTemplate()
19 | .Title(title)
20 | .Body(body)
21 | .Doc()
22 | )
23 |
24 | []
25 | let RouteClientPage () =
26 | let router = Routes.InstallRouter ()
27 |
28 | router.View
29 | |> View.Map (fun endpoint ->
30 | match endpoint with
31 | | EndPoint.Home ->
32 | PageHome.Main router
33 |
34 | | EndPoint.Login ->
35 | PageLogin.Main router
36 |
37 | | EndPoint.Listing ->
38 | PageListing.Main router
39 |
40 | | EndPoint.Form code ->
41 | PageForm.Main router code
42 |
43 | | _ ->
44 | div [] [ text "implementation pending" ]
45 | )
46 | |> Doc.EmbedView
47 |
48 | let LoadClientPage ctx title endpoint =
49 | let body = client <@ RouteClientPage() @>
50 | MainTemplate ctx endpoint title [ body ]
51 |
52 | []
53 | let Main =
54 | Sitelet.New
55 | SiteRouter
56 | (fun ctx endpoint ->
57 | let loggedUser =
58 | async {
59 | return! ctx.UserSession.GetLoggedInUser()
60 | }
61 | |> Async.RunSynchronously
62 |
63 | match loggedUser with
64 | | None -> // user is not authenticated. Allow only public EndPoints
65 | match endpoint with
66 | | EndPoint.Home ->
67 | LoadClientPage ctx "Home" endpoint
68 |
69 | | EndPoint.Login ->
70 | LoadClientPage ctx "Login" endpoint
71 |
72 | | EndPoint.AccessDenied ->
73 | MainTemplate ctx endpoint "Access Denied Page"
74 | [ div [] [ text "Access denied" ] ]
75 | | _ ->
76 | Content.RedirectTemporary AccessDenied
77 |
78 | | Some (u) -> // user is authenticated. Allow all EndPoints
79 | match endpoint with
80 | | EndPoint.Home ->
81 | LoadClientPage ctx "Home" endpoint
82 |
83 | | EndPoint.Login ->
84 | LoadClientPage ctx "Login" endpoint
85 |
86 | | EndPoint.Listing ->
87 | LoadClientPage ctx "Listing Page" endpoint
88 |
89 | | EndPoint.Form code ->
90 | LoadClientPage ctx "Form Page" endpoint
91 |
92 | | _ ->
93 | MainTemplate ctx endpoint "not implemented"
94 | [ div [] [ text "implementation pending" ] ]
95 | )
96 |
97 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/Page.Form.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd.Pages
2 |
3 | open WebSharper
4 | open WebSharper.UI
5 | open WebSharper.UI.Client
6 | open WebSharper.UI.Html
7 | open WebSharper.JavaScript
8 |
9 | open WebSharperTutorial.FrontEnd
10 | open WebSharperTutorial.FrontEnd.Components
11 |
12 | []
13 | module PageForm =
14 |
15 | type private formTemplate = Templating.Template<"templates/Page.Form.html">
16 |
17 | let private AlertBox (rvStatusMsg:Var) =
18 | rvStatusMsg.View
19 | |> View.Map (fun msgO ->
20 | match msgO with
21 | | None ->
22 | Doc.Empty
23 | | Some msg ->
24 | div [ attr.``class`` "alert alert-primary"
25 | Attr.Create "role" "alert"
26 | ]
27 | [ text msg ]
28 | )
29 | |> Doc.EmbedView
30 |
31 | let private spinner msg =
32 | div
33 | [ attr.``class`` "spinner-border text-warning"
34 | Attr.Create "role" "status"
35 | ]
36 | [ span [ attr.``class`` "sr-only" ] [ text msg ]
37 | ]
38 |
39 | let private frameContent navBar content =
40 | [
41 | navBar
42 | div [ attr.``class`` "container" ]
43 | [
44 | div [ attr.``class`` "row" ]
45 | [ div [ attr.``class`` "col-12" ]
46 | [ content ]
47 | ]
48 | ]
49 | ]
50 | |> Doc.Concat
51 |
52 | let Main router code =
53 | let rvStatusMsg = Var.Create None
54 | let statusMsgBox = AlertBox rvStatusMsg
55 |
56 | let rvModel = Var.CreateWaiting()
57 | let submitter =
58 | Submitter.CreateOption rvModel.View
59 |
60 | let loadModel() =
61 | async {
62 | let! modelR =
63 | Server.GetUser code
64 |
65 | match modelR with
66 | | Error error ->
67 | Var.Set rvStatusMsg (Some error)
68 |
69 | | Ok model ->
70 | Var.Set rvModel model
71 | submitter.Trigger()
72 |
73 | return ()
74 | }
75 |
76 | let navBar =
77 | NavigationBar.Main router
78 |
79 | let content =
80 | submitter.View
81 | |> View.Map (fun modelO ->
82 | match modelO with
83 | | None -> spinner "loading..."
84 | | Some model ->
85 | let rvCode =
86 | rvModel.Lens
87 | (fun model -> string model.Code)
88 | (fun model value -> { model with Code = int64 value })
89 |
90 | formTemplate()
91 | .AlertBox(statusMsgBox)
92 | .Code(rvCode)
93 | .Firstname(Lens(rvModel.V.Firstname))
94 | .Lastname(Lens(rvModel.V.Lastname))
95 | .UpdatedAt(model.UpdateDate.ToShortDateString())
96 | .OnSave(fun evt ->
97 | async {
98 | let! modelR =
99 | Server.SaveUser rvModel.Value
100 |
101 | match modelR with
102 | | Error error ->
103 | Var.Set rvStatusMsg (Some error)
104 |
105 | | Ok model ->
106 | Var.Set rvModel model
107 | Var.Set rvStatusMsg (Some "Saved!")
108 | submitter.Trigger()
109 | }
110 | |> Async.Start
111 | )
112 | .OnBack(fun _ ->
113 | Var.Set router Routes.Listing
114 | )
115 | .Doc()
116 |
117 | )
118 | |> Doc.EmbedView
119 |
120 | loadModel()
121 | |> Async.Start
122 |
123 | frameContent navBar content
124 |
125 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/Page.Home.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd.Pages
2 |
3 | open WebSharper
4 | open WebSharper.UI
5 | open WebSharper.UI.Client
6 | open WebSharper.UI.Html
7 | open WebSharper.JQuery
8 | open WebSharper.JavaScript // require by the Remote<'T> type
9 |
10 | open WebSharperTutorial.FrontEnd
11 | open WebSharperTutorial.FrontEnd.Components
12 |
13 | []
14 | module PageHome =
15 |
16 | let Main router =
17 | let navBar =
18 | NavigationBar.Main router
19 |
20 | [
21 | navBar
22 | div [ attr.``class`` "container" ]
23 | [
24 | div [ attr.``class`` "row" ]
25 | [ div [ attr.``class`` "col-xs-12 col-sm-6 mx-auto" ]
26 | [ text "this is the home page" ]
27 | ]
28 | ]
29 | ]
30 | |> Doc.Concat
31 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/Page.Listing.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd.Pages
2 |
3 | open WebSharper
4 | open WebSharper.UI
5 | open WebSharper.UI.Client
6 | open WebSharper.UI.Html
7 |
8 | open WebSharperTutorial.FrontEnd
9 | open WebSharperTutorial.FrontEnd.Components
10 |
11 | []
12 | module PageListing =
13 |
14 | type private listingTemplate = Templating.Template<"templates/Page.Listing.html">
15 |
16 | let private buildTable router (users:DTO.User list) =
17 | let tableRows =
18 | users
19 | |> List.map(fun user ->
20 | listingTemplate.RowTemplate()
21 | .Code(string user.Code)
22 | .Firstname(user.Firstname)
23 | .Lastname(user.Lastname)
24 | .UpdatedAt(user.UpdateDate.ToShortDateString())
25 | .OnEdit(fun _ -> Var.Set router (Routes.Form user.Code))
26 | .Doc()
27 | )
28 |
29 | listingTemplate()
30 | .Rows(tableRows)
31 | .Doc()
32 |
33 | let Main router =
34 | async {
35 | let navBar =
36 | NavigationBar.Main router
37 |
38 | let! users =
39 | Server.GetUsers()
40 |
41 | let tableElement =
42 | buildTable router users
43 |
44 | return
45 | [
46 | navBar
47 | div [ attr.``class`` "container" ]
48 | [
49 | div [ attr.``class`` "row" ]
50 | [ div [ attr.``class`` "col-12" ]
51 | [ tableElement ]
52 | ]
53 | ]
54 | ]
55 | |> Doc.Concat
56 | }
57 | |> Doc.Async
58 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/Page.Login.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd.Pages
2 |
3 | open WebSharper
4 | open WebSharper.UI
5 | open WebSharper.UI.Client
6 | open WebSharper.UI.Html
7 | open WebSharper.JQuery
8 | open WebSharper.JavaScript // require by the Remote<'T> type
9 |
10 | open WebSharperTutorial.FrontEnd
11 | open WebSharperTutorial.FrontEnd.Components
12 |
13 | []
14 | module PageLogin =
15 |
16 | type private loginFormTemplate = Templating.Template<"templates/Page.Login.html">
17 |
18 | let private AlertBox (rvStatusMsg:Var) =
19 | rvStatusMsg.View
20 | |> View.Map (fun msgO ->
21 | match msgO with
22 | | None ->
23 | Doc.Empty
24 | | Some msg ->
25 | div [ attr.``class`` "alert alert-primary"
26 | Attr.Create "role" "alert"
27 | ]
28 | [ text msg ]
29 | )
30 | |> Doc.EmbedView
31 |
32 | let private FormLogin (router:Var) =
33 | let rvEmail = Var.Create ""
34 | let rvPassword = Var.Create ""
35 | let rvKeepLogged = Var.Create true
36 | let rvStatusMsg = Var.Create None
37 |
38 | let statusMsgBox = AlertBox rvStatusMsg
39 |
40 | loginFormTemplate()
41 | .AlertBox(statusMsgBox)
42 | .Login(rvEmail)
43 | .Password(rvPassword)
44 | .RememberMe(rvKeepLogged)
45 | .OnLogin(fun _ ->
46 | JQuery.Of("form").One("submit", fun elem ev -> ev.PreventDefault()).Ignore
47 | async {
48 | let! response =
49 | Remote.CheckCredentials rvEmail.Value rvPassword.Value rvKeepLogged.Value
50 | match response with
51 | | Result.Ok c ->
52 | rvEmail.Value <- ""
53 | rvPassword.Value <- ""
54 | rvStatusMsg.Value <- None
55 | router.Value <- Routes.Listing
56 |
57 | | Result.Error error ->
58 | rvStatusMsg.Value <- Some error
59 | }
60 | |> Async.Start
61 | )
62 | .OnLogout(fun _ ->
63 | async {
64 | do! Remote.Logout ()
65 | Var.Set router Routes.Home
66 | }
67 | |> Async.Start
68 | )
69 | .Doc()
70 |
71 | let Main router =
72 | let formLogin = FormLogin router
73 | let navBar =
74 | NavigationBar.Main router
75 |
76 | [
77 | navBar
78 | div [ attr.``class`` "container" ]
79 | [
80 | div [ attr.``class`` "row" ]
81 | [ div [ attr.``class`` "col-xs-12 col-sm-6 mx-auto" ] [ formLogin ]
82 | ]
83 | ]
84 | ]
85 | |> Doc.Concat
86 |
87 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/Resources.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 | open WebSharper
5 | open WebSharper.Resources
6 |
7 | module AppResources =
8 |
9 | module Bootstrap =
10 | [)>]
11 | type Js() =
12 | inherit BaseResource("/vendor/bootstrap/js/bootstrap.bundle.min.js")
13 | type Css() =
14 | inherit BaseResource("/vendor/bootstrap/css/bootstrap.min.css")
15 |
16 | module FrontEndApp =
17 | type Css() =
18 | inherit BaseResource("/app/css/common.css")
19 |
20 | type Js() =
21 | inherit BaseResource("/app/js/common.js")
22 |
23 | [);
24 | assembly:Require(typeof);
25 | assembly:Require(typeof);
26 | assembly:Require(typeof);
27 | >]
28 | do()
29 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/Routes.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 | open WebSharper
5 | open WebSharper.Sitelets
6 | open WebSharper.UI
7 |
8 | module Routes =
9 |
10 | []
11 | type EndPoint =
12 | | [] Home
13 | | [] Login
14 | | [] AccessDenied
15 | | [] Listing
16 | | [] Form of int64
17 |
18 | (* Router is used by both client and server side *)
19 | []
20 | let SiteRouter : Router =
21 | let link endPoint =
22 | match endPoint with
23 | | Home -> [ ]
24 | | Login -> [ "login" ]
25 | | AccessDenied -> [ "access-denied" ]
26 | | Listing -> [ "private"; "listing" ]
27 | | Form code -> [ "private"; "form"; string code ]
28 |
29 | let route (path) =
30 | match path with
31 | | [ ] -> Some Home
32 | | [ "login" ] -> Some Login
33 | | [ "access-denied" ] -> Some AccessDenied
34 | | [ "private"; "listing" ] -> Some Listing
35 | | [ "private"; "form"; code ] -> Some (Form (int64 code))
36 | | _ -> None
37 |
38 | Router.Create link route
39 |
40 | []
41 | let InstallRouter () =
42 | let router =
43 | SiteRouter
44 | |> Router.Slice
45 | (fun endpoint ->
46 | (* Turn off client side routing for AccessDenied endpoint *)
47 | match endpoint with
48 | | AccessDenied -> None
49 | | _ -> Some endpoint
50 | )
51 | id
52 | |> Router.Install Home
53 | router
54 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/Server.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 |
5 | open WebSharper
6 |
7 | open WebSharperTutorial.FrontEnd
8 | open WebSharperTutorial.FrontEnd.DTO
9 |
10 | module Server =
11 |
12 | let private dbUsers () =
13 | [
14 | CreateUser 1L "Firstname 1" "Lastname 1" (new DateTime(2020,3,17))
15 | CreateUser 2L "Firstname 2" "Lastname 2" (new DateTime(2019,6,21))
16 | CreateUser 3L "Firstname 3" "Lastname 3" (new DateTime(2019,8,14))
17 | ]
18 |
19 | []
20 | let GetUsers () : Async =
21 | async {
22 | return dbUsers()
23 | }
24 |
25 | let private optionToResult msg o =
26 | match o with
27 | | None -> Result.Error msg
28 | | Some v -> Result.Ok v
29 |
30 | let private validateFirstname (user:User) =
31 | match user.Firstname with
32 | | null -> Error "No fistname found."
33 | | "" -> Error "Fistname is empty."
34 | | _ -> Ok user
35 |
36 | let private validateLastname (user:User) =
37 | match user.Firstname with
38 | | null -> Error "No lastname found."
39 | | "" -> Error "Lastname is empty."
40 | | _ -> Ok user
41 |
42 | let private validateRequest userResult =
43 | userResult
44 | |> Result.bind validateFirstname
45 | |> Result.bind validateLastname
46 |
47 | let private updateUser user =
48 | { user with
49 | UpdateDate = DateTime.Now
50 | }
51 |
52 | []
53 | let SaveUser (dto:User) : Async> =
54 | async {
55 | return
56 | dto
57 | |> Ok
58 | |> validateRequest
59 | |> Result.map updateUser
60 | }
61 |
62 | []
63 | let GetUser (code:int64) : Async> =
64 | async {
65 | // simulate delay
66 | do! Async.Sleep(2000)
67 |
68 | let userO =
69 | dbUsers()
70 | |> List.tryFind(fun u -> u.Code = code)
71 | |> optionToResult "User not found!"
72 |
73 | return userO
74 | }
75 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/Startup.fs:
--------------------------------------------------------------------------------
1 | namespace WebSharperTutorial.FrontEnd
2 |
3 | open System
4 | open Microsoft.AspNetCore
5 | open Microsoft.AspNetCore.Builder
6 | open Microsoft.AspNetCore.Hosting
7 | open Microsoft.AspNetCore.Http
8 | open Microsoft.Extensions.Configuration
9 | open Microsoft.Extensions.DependencyInjection
10 | open Microsoft.Extensions.Hosting
11 | open WebSharper.AspNetCore
12 |
13 | type Startup() =
14 |
15 | member this.ConfigureServices(services: IServiceCollection) =
16 | services.AddSitelet(Site.Main)
17 | .AddWebSharperRemoting() // <-- add this line
18 | .AddAuthentication("WebSharper")
19 | .AddCookie("WebSharper", fun options -> ())
20 | |> ignore
21 |
22 | member this.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =
23 | if env.IsDevelopment() then app.UseDeveloperExceptionPage() |> ignore
24 |
25 | app.UseAuthentication()
26 | .UseStaticFiles()
27 | .UseWebSharper()
28 | .Run(fun context ->
29 | context.Response.StatusCode <- 404
30 | context.Response.WriteAsync("Page not found"))
31 |
32 | module Program =
33 | let BuildWebHost args =
34 | WebHost
35 | .CreateDefaultBuilder(args)
36 | .UseStartup()
37 | .Build()
38 |
39 | []
40 | let main args =
41 | BuildWebHost(args).Run()
42 | 0
43 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/WebSharperTutorial.FrontEnd.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "websharper": {
3 | "WebSharper.JQuery.Resources.JQuery": "https://code.jquery.com/jquery-3.2.1.min.js"
4 | }
5 | }
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/templates/Main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | ${Title}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/templates/Page.Form.html:
--------------------------------------------------------------------------------
1 |
37 |
38 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/templates/Page.Listing.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #
5 | First
6 | Last
7 | Updated At
8 |
9 |
10 |
11 |
12 | ${Code}
13 | ${Firstname}
14 | ${Lastname}
15 | ${UpdatedAt}
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/templates/Page.Login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Provide your credentials
4 |
5 |
6 |
7 |
8 |
28 |
29 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/wsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://websharper.com/wsconfig.schema.json",
3 | "project": "site",
4 | "outputDir": "wwwroot"
5 | }
6 |
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/wwwroot/app/css/common.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/code/chapter-06/WebSharperTutorial.FrontEnd/wwwroot/app/css/common.css
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/wwwroot/app/js/common.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexPeret/websharper-cookbook-tutorial/b25e7f5f3c5ec9b635c79b9aef60983a16b7ee50/code/chapter-06/WebSharperTutorial.FrontEnd/wwwroot/app/js/common.js
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/wwwroot/vendor/bootstrap/css/bootstrap-reboot.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v4.5.0 (https://getbootstrap.com/)
3 | * Copyright 2011-2020 The Bootstrap Authors
4 | * Copyright 2011-2020 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */
8 | *,
9 | *::before,
10 | *::after {
11 | box-sizing: border-box;
12 | }
13 |
14 | html {
15 | font-family: sans-serif;
16 | line-height: 1.15;
17 | -webkit-text-size-adjust: 100%;
18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
19 | }
20 |
21 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
22 | display: block;
23 | }
24 |
25 | body {
26 | margin: 0;
27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
28 | font-size: 1rem;
29 | font-weight: 400;
30 | line-height: 1.5;
31 | color: #212529;
32 | text-align: left;
33 | background-color: #fff;
34 | }
35 |
36 | [tabindex="-1"]:focus:not(:focus-visible) {
37 | outline: 0 !important;
38 | }
39 |
40 | hr {
41 | box-sizing: content-box;
42 | height: 0;
43 | overflow: visible;
44 | }
45 |
46 | h1, h2, h3, h4, h5, h6 {
47 | margin-top: 0;
48 | margin-bottom: 0.5rem;
49 | }
50 |
51 | p {
52 | margin-top: 0;
53 | margin-bottom: 1rem;
54 | }
55 |
56 | abbr[title],
57 | abbr[data-original-title] {
58 | text-decoration: underline;
59 | -webkit-text-decoration: underline dotted;
60 | text-decoration: underline dotted;
61 | cursor: help;
62 | border-bottom: 0;
63 | -webkit-text-decoration-skip-ink: none;
64 | text-decoration-skip-ink: none;
65 | }
66 |
67 | address {
68 | margin-bottom: 1rem;
69 | font-style: normal;
70 | line-height: inherit;
71 | }
72 |
73 | ol,
74 | ul,
75 | dl {
76 | margin-top: 0;
77 | margin-bottom: 1rem;
78 | }
79 |
80 | ol ol,
81 | ul ul,
82 | ol ul,
83 | ul ol {
84 | margin-bottom: 0;
85 | }
86 |
87 | dt {
88 | font-weight: 700;
89 | }
90 |
91 | dd {
92 | margin-bottom: .5rem;
93 | margin-left: 0;
94 | }
95 |
96 | blockquote {
97 | margin: 0 0 1rem;
98 | }
99 |
100 | b,
101 | strong {
102 | font-weight: bolder;
103 | }
104 |
105 | small {
106 | font-size: 80%;
107 | }
108 |
109 | sub,
110 | sup {
111 | position: relative;
112 | font-size: 75%;
113 | line-height: 0;
114 | vertical-align: baseline;
115 | }
116 |
117 | sub {
118 | bottom: -.25em;
119 | }
120 |
121 | sup {
122 | top: -.5em;
123 | }
124 |
125 | a {
126 | color: #007bff;
127 | text-decoration: none;
128 | background-color: transparent;
129 | }
130 |
131 | a:hover {
132 | color: #0056b3;
133 | text-decoration: underline;
134 | }
135 |
136 | a:not([href]) {
137 | color: inherit;
138 | text-decoration: none;
139 | }
140 |
141 | a:not([href]):hover {
142 | color: inherit;
143 | text-decoration: none;
144 | }
145 |
146 | pre,
147 | code,
148 | kbd,
149 | samp {
150 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
151 | font-size: 1em;
152 | }
153 |
154 | pre {
155 | margin-top: 0;
156 | margin-bottom: 1rem;
157 | overflow: auto;
158 | -ms-overflow-style: scrollbar;
159 | }
160 |
161 | figure {
162 | margin: 0 0 1rem;
163 | }
164 |
165 | img {
166 | vertical-align: middle;
167 | border-style: none;
168 | }
169 |
170 | svg {
171 | overflow: hidden;
172 | vertical-align: middle;
173 | }
174 |
175 | table {
176 | border-collapse: collapse;
177 | }
178 |
179 | caption {
180 | padding-top: 0.75rem;
181 | padding-bottom: 0.75rem;
182 | color: #6c757d;
183 | text-align: left;
184 | caption-side: bottom;
185 | }
186 |
187 | th {
188 | text-align: inherit;
189 | }
190 |
191 | label {
192 | display: inline-block;
193 | margin-bottom: 0.5rem;
194 | }
195 |
196 | button {
197 | border-radius: 0;
198 | }
199 |
200 | button:focus {
201 | outline: 1px dotted;
202 | outline: 5px auto -webkit-focus-ring-color;
203 | }
204 |
205 | input,
206 | button,
207 | select,
208 | optgroup,
209 | textarea {
210 | margin: 0;
211 | font-family: inherit;
212 | font-size: inherit;
213 | line-height: inherit;
214 | }
215 |
216 | button,
217 | input {
218 | overflow: visible;
219 | }
220 |
221 | button,
222 | select {
223 | text-transform: none;
224 | }
225 |
226 | [role="button"] {
227 | cursor: pointer;
228 | }
229 |
230 | select {
231 | word-wrap: normal;
232 | }
233 |
234 | button,
235 | [type="button"],
236 | [type="reset"],
237 | [type="submit"] {
238 | -webkit-appearance: button;
239 | }
240 |
241 | button:not(:disabled),
242 | [type="button"]:not(:disabled),
243 | [type="reset"]:not(:disabled),
244 | [type="submit"]:not(:disabled) {
245 | cursor: pointer;
246 | }
247 |
248 | button::-moz-focus-inner,
249 | [type="button"]::-moz-focus-inner,
250 | [type="reset"]::-moz-focus-inner,
251 | [type="submit"]::-moz-focus-inner {
252 | padding: 0;
253 | border-style: none;
254 | }
255 |
256 | input[type="radio"],
257 | input[type="checkbox"] {
258 | box-sizing: border-box;
259 | padding: 0;
260 | }
261 |
262 | textarea {
263 | overflow: auto;
264 | resize: vertical;
265 | }
266 |
267 | fieldset {
268 | min-width: 0;
269 | padding: 0;
270 | margin: 0;
271 | border: 0;
272 | }
273 |
274 | legend {
275 | display: block;
276 | width: 100%;
277 | max-width: 100%;
278 | padding: 0;
279 | margin-bottom: .5rem;
280 | font-size: 1.5rem;
281 | line-height: inherit;
282 | color: inherit;
283 | white-space: normal;
284 | }
285 |
286 | progress {
287 | vertical-align: baseline;
288 | }
289 |
290 | [type="number"]::-webkit-inner-spin-button,
291 | [type="number"]::-webkit-outer-spin-button {
292 | height: auto;
293 | }
294 |
295 | [type="search"] {
296 | outline-offset: -2px;
297 | -webkit-appearance: none;
298 | }
299 |
300 | [type="search"]::-webkit-search-decoration {
301 | -webkit-appearance: none;
302 | }
303 |
304 | ::-webkit-file-upload-button {
305 | font: inherit;
306 | -webkit-appearance: button;
307 | }
308 |
309 | output {
310 | display: inline-block;
311 | }
312 |
313 | summary {
314 | display: list-item;
315 | cursor: pointer;
316 | }
317 |
318 | template {
319 | display: none;
320 | }
321 |
322 | [hidden] {
323 | display: none !important;
324 | }
325 | /*# sourceMappingURL=bootstrap-reboot.css.map */
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.FrontEnd/wwwroot/vendor/bootstrap/css/bootstrap-reboot.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v4.5.0 (https://getbootstrap.com/)
3 | * Copyright 2011-2020 The Bootstrap Authors
4 | * Copyright 2011-2020 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */
--------------------------------------------------------------------------------
/code/chapter-06/WebSharperTutorial.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26124.0
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WebSharperTutorial.FrontEnd", "WebSharperTutorial.FrontEnd\WebSharperTutorial.FrontEnd.fsproj", "{6870CF92-C520-4FC9-8814-C93AECA5F5D6}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Debug|x64 = Debug|x64
12 | Debug|x86 = Debug|x86
13 | Release|Any CPU = Release|Any CPU
14 | Release|x64 = Release|x64
15 | Release|x86 = Release|x86
16 | EndGlobalSection
17 | GlobalSection(SolutionProperties) = preSolution
18 | HideSolutionNode = FALSE
19 | EndGlobalSection
20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
21 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
22 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
23 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x64.ActiveCfg = Debug|Any CPU
24 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x64.Build.0 = Debug|Any CPU
25 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x86.ActiveCfg = Debug|Any CPU
26 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Debug|x86.Build.0 = Debug|Any CPU
27 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|Any CPU.Build.0 = Release|Any CPU
29 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x64.ActiveCfg = Release|Any CPU
30 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x64.Build.0 = Release|Any CPU
31 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x86.ActiveCfg = Release|Any CPU
32 | {6870CF92-C520-4FC9-8814-C93AECA5F5D6}.Release|x86.Build.0 = Release|Any CPU
33 | EndGlobalSection
34 | EndGlobal
35 |
--------------------------------------------------------------------------------