├── .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 | <Project Sdk="Microsoft.NET.Sdk.Web"> 199 | 200 | <PropertyGroup> 201 | <TargetFramework>netcoreapp3.1</TargetFramework> 202 | </PropertyGroup> 203 | 204 | <ItemGroup> 205 | <Content Include="templates/Main.html" CopyToPublishDirectory="Always" /> 206 | <Compile Include="Main.fs" /> 207 | <Compile Include="Startup.fs" /> 208 | <None Include="wsconfig.json" /> 209 | </ItemGroup> 210 | 211 | <ItemGroup> 212 | <PackageReference Include="WebSharper" Version="4.6.6.407" /> 213 | <PackageReference Include="WebSharper.FSharp" Version="4.6.6.407" /> 214 | <PackageReference Include="WebSharper.UI" Version="4.6.3.219" /> 215 | <PackageReference Include="WebSharper.AspNetCore" Version="4.6.2.136" /> 216 | </ItemGroup> 217 | 218 | </Project> 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 | | [<EndPoint "/">] 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 | [<Website>] 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<a id="sec-1"></a> 6 | 7 | ## the routing system<a id="sec-1-1"></a> 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 | ![img](./images/cookbook-chapter-02-image-01.png "The home and access denied pages") 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 | <ItemGroup> 23 | ... 24 | <Compile Include="Routes.fs" /> 25 | <Compile Include="Main.fs" /> 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 | [<JavaScript>] 42 | type EndPoint = 43 | | [<EndPoint>] Home 44 | | [<EndPoint>] Login 45 | | [<EndPoint>] AccessDenied 46 | | [<EndPoint>] Listing 47 | | [<EndPoint>] 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 ` [<JavaScript>] ` attribute at the **EndPoint** type. This attribute is used by WebSharper compiler to transpile this type onto the JavaScript equivalent one. 54 | 55 | The ` [<EndPoint>] ` 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. ` [<EndPoint "POST /private/form">] ` 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 | [<JavaScript>] 66 | let SiteRouter : Router<EndPoint> = 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 | [<JavaScript>] 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<EndPoint>` 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 | | [<EndPoint "/">] 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 | [<Website>] 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. <http://localhost:5000/access-denied>). 147 | 148 | ## embedding resources<a id="sec-1-2"></a> 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 `<project-path>/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 | [<Require(typeof<JQuery.Resources.JQuery>)>] 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 | [<assembly:Require(typeof<Bootstrap.Js>); 186 | assembly:Require(typeof<Bootstrap.Css>); 187 | assembly:Require(typeof<FrontEndApp.Css>); 188 | assembly:Require(typeof<FrontEndApp.Js>); 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 `<project-path>/wwwroot/app/`. 195 | 196 | The last step is to remove the reference from the ***template/Main.html*** file 197 | 198 | ```html 199 | <!-- remove the lines below --> 200 | 201 | <!-- Bootstrap CSS --> 202 | <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> 203 | 204 | ... 205 | <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script> 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 | ![img](./images/cookbook-chapter-02-image-02.png "Inspector: the resources files weren't load.") 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 | ![img](./images/cookbook-chapter-02-image-03.png "Inspector: the resources files were load.") 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<a id="sec-1"></a> 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 | ![img](./images/cookbook-chapter-05-image-01.png "The listing page") 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 | <table class="table table-striped"> 22 | <thead> 23 | <tr> 24 | <th scope="col">#</th> 25 | <th scope="col">First</th> 26 | <th scope="col">Last</th> 27 | <th scope="col">Updated At</th> 28 | </tr> 29 | </thead> 30 | <tbody ws-hole="Rows"> 31 | <tr ws-template="RowTemplate"> 32 | <th scope="row">${Code}</th> 33 | <td><a class="btn btn-link" href="javascript:void(0)" ws-onclick="OnEdit">${Firstname}</a></td> 34 | <td>${Lastname}</td> 35 | <td>${UpdatedAt}</td> 36 | </tr> 37 | </tbody> 38 | </table> 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 | [<JavaScript>] 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 | [<JavaScript>] 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 `[<JavaScript>]` 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 | [<Rpc>] 170 | let GetUsers () : Async<User list> = 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 | [<JavaScript>] 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 | <table class="table table-striped"> 24 | <thead> 25 | <tr> 26 | <th scope="col">#</th> 27 | <th scope="col">First</th> 28 | <th scope="col">Last</th> 29 | <th scope="col">Updated At</th> 30 | </tr> 31 | </thead> 32 | <tbody ws-hole="Rows"> 33 | <tr ws-template="RowTemplate"> 34 | <th scope="row">${Code}</th> 35 | <td><a class="btn btn-link" href="javascript:void(0)" ws-onclick="OnEdit">${Firstname}</a></td> 36 | <td>${Lastname}</td> 37 | <td>${UpdatedAt}</td> 38 | </tr> 39 | </tbody> 40 | </table> 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 | [<JavaScript>] 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 | [<JavaScript>] 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]{[<JavaScript>]} 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 | [<Rpc>] 180 | let GetUsers () : Async<User list> = 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 | [<JavaScript>] 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<a id="sec-1"></a> 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 | | [<EndPoint "/">] 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 | [<Website>] 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<Startup>() 36 | .Build() 37 | 38 | [<EntryPoint>] 39 | let main args = 40 | BuildWebHost(args).Run() 41 | 0 42 | -------------------------------------------------------------------------------- /code/chapter-01/WebSharperTutorial.FrontEnd/WebSharperTutorial.FrontEnd.fsproj: -------------------------------------------------------------------------------- 1 | <Project Sdk="Microsoft.NET.Sdk.Web"> 2 | 3 | <PropertyGroup> 4 | <TargetFramework>netcoreapp3.1</TargetFramework> 5 | </PropertyGroup> 6 | 7 | <ItemGroup> 8 | <Content Include="Main.html" CopyToPublishDirectory="Always" /> 9 | <Compile Include="Main.fs" /> 10 | <Compile Include="Startup.fs" /> 11 | <None Include="wsconfig.json" /> 12 | </ItemGroup> 13 | 14 | <ItemGroup> 15 | <PackageReference Include="WebSharper" Version="4.6.6.407" /> 16 | <PackageReference Include="WebSharper.FSharp" Version="4.6.6.407" /> 17 | <PackageReference Include="WebSharper.UI" Version="4.6.3.219" /> 18 | <PackageReference Include="WebSharper.AspNetCore" Version="4.6.2.136" /> 19 | </ItemGroup> 20 | 21 | </Project> -------------------------------------------------------------------------------- /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 | <!doctype html> 2 | <html lang="en"> 3 | <head> 4 | <!-- Required meta tags --> 5 | <meta charset="utf-8"> 6 | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 7 | 8 | <!-- Bootstrap CSS --> 9 | <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> 10 | 11 | <title>${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 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | Login 23 | 24 | 25 | Logout 26 | 27 |
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 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | Login 23 | 24 | 25 | Logout 26 | 27 |
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 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
#FirstLastUpdated At
${Code}${Firstname}${Lastname}${UpdatedAt}
19 | -------------------------------------------------------------------------------- /code/chapter-05/WebSharperTutorial.FrontEnd/templates/Page.Login.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Provide your credentials

4 |
5 | 6 | 7 | 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | Login 23 | 24 | 25 | Logout 26 | 27 |
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 |
2 | 3 | 4 |
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 |
37 | 38 | -------------------------------------------------------------------------------- /code/chapter-06/WebSharperTutorial.FrontEnd/templates/Page.Listing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
#FirstLastUpdated At
${Code}${Firstname}${Lastname}${UpdatedAt}
19 | -------------------------------------------------------------------------------- /code/chapter-06/WebSharperTutorial.FrontEnd/templates/Page.Login.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Provide your credentials

4 |
5 | 6 | 7 | 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | Login 23 | 24 | 25 | Logout 26 | 27 |
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 | --------------------------------------------------------------------------------