├── src └── Bolero.TodoMVC.Client │ ├── paket.references │ ├── Startup.fs │ ├── Bolero.TodoMVC.Client.fsproj │ ├── Properties │ └── launchSettings.json │ ├── wwwroot │ ├── index.html │ ├── base.css │ └── index.css │ ├── template.html │ └── Main.fs ├── .gitignore ├── .config └── dotnet-tools.json ├── README.md ├── paket.dependencies ├── appveyor.yml ├── tools └── gh-pages.ps1 ├── Bolero.TodoMVC.sln ├── paket.lock └── .paket └── Paket.Restore.targets /src/Bolero.TodoMVC.Client/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | Microsoft.AspNetCore.Components.WebAssembly.DevServer 3 | Bolero 4 | Bolero.Build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | .vs/ 4 | *.user 5 | paket-files/ 6 | .paket/* 7 | !.paket/paket.bootstrapper.proj 8 | !.paket/paket.bootstrapper.props 9 | !.paket/Paket.Restore.targets 10 | publish/ 11 | -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "paket": { 6 | "version": "7.1.5", 7 | "commands": [ 8 | "paket" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bolero TodoMVC 2 | 3 | This is an implementation of the [TodoMVC](http://todomvc.com/) sample application using [Bolero](https://github.com/fsbolero/Bolero). 4 | 5 | [See it running live here.](https://fsbolero.github.io/TodoMVC/) 6 | -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | source https://api.nuget.org/v3/index.json 2 | # source https://nuget.pkg.github.com/fsbolero/index.json 3 | storage: none 4 | framework: net6.0 5 | 6 | nuget Microsoft.AspNetCore.Components.WebAssembly.DevServer 7 | nuget Bolero 8 | nuget Bolero.Build -------------------------------------------------------------------------------- /src/Bolero.TodoMVC.Client/Startup.fs: -------------------------------------------------------------------------------- 1 | namespace Bolero.TodoMVC.Client 2 | 3 | open Microsoft.AspNetCore.Components.WebAssembly.Hosting 4 | open Bolero.Remoting.Client 5 | 6 | module Program = 7 | 8 | [] 9 | let Main args = 10 | let builder = WebAssemblyHostBuilder.CreateDefault(args) 11 | builder.RootComponents.Add(".todoapp") 12 | builder.Services.AddRemoting(builder.HostEnvironment) |> ignore 13 | builder.Build().RunAsync() |> ignore 14 | 0 15 | -------------------------------------------------------------------------------- /src/Bolero.TodoMVC.Client/Bolero.TodoMVC.Client.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: build {build} 2 | 3 | image: 4 | - Ubuntu 5 | 6 | build: 7 | verbosity: minimal 8 | 9 | branches: 10 | only: 11 | - master 12 | 13 | init: 14 | - git config --global core.autocrlf input 15 | 16 | build_script: 17 | - dotnet tool restore 18 | - dotnet publish src/Bolero.TodoMVC.Client/ -o $(pwd)/publish/ -r Portable 19 | 20 | on_success: 21 | - pwsh -file ./tools/gh-pages.ps1 -env appveyor 22 | 23 | environment: 24 | GH_TOKEN: 25 | secure: dhFy1eZoqG4QPkKTuLFxix7QQMxaIyIpYjkvTU3CukYZz1CEOJeHfBSaT8tkPsGL 26 | -------------------------------------------------------------------------------- /src/Bolero.TodoMVC.Client/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:54254/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Bolero.TodoMVC.Client": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:54257/" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Bolero.TodoMVC.Client/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TodoMVC with Bolero 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tools/gh-pages.ps1: -------------------------------------------------------------------------------- 1 | # pushes src/wwwroot to gh-pages branch 2 | 3 | param ([string] $env = "local") 4 | 5 | $msg = 'gh-pages.ps1: tests/client/wwwroot -> gh-pages' 6 | $gitURL = "https://github.com/fsbolero/TodoMVC" 7 | 8 | write-host -foregroundColor "green" "=====> $msg" 9 | 10 | function clearDir() { 11 | remove-item -r build/gh-pages -errorAction ignore 12 | } 13 | 14 | if ($env -eq "appveyor") { 15 | clearDir 16 | $d = mkdir -force build 17 | git clone $gitURL build/gh-pages -b gh-pages --single-branch 18 | cd build/gh-pages 19 | git config credential.helper "store --file=.git/credentials" 20 | $t = $env:GH_TOKEN 21 | $cred = "https://" + $t + ":@github.com" 22 | $d = pwd 23 | [System.IO.File]::WriteAllText("$pwd/.git/credentials", $cred) 24 | git config user.name "AppVeyor" 25 | git config user.email "websharper-support@intellifactory.com" 26 | } else { 27 | clearDir 28 | cd build 29 | git clone .. gh-pages -b gh-pages --single-branch 30 | cd gh-pages 31 | } 32 | 33 | git rm -rf * 34 | copy-item -r -force ../../publish/wwwroot/* . 35 | echo $null >> .nojekyll 36 | echo "* -crlf" > .gitattributes 37 | (get-content '.\index.html' -encoding utf8).replace('git.log 39 | git commit --amend -am $msg 40 | git push -f -u origin gh-pages 41 | cd ../.. 42 | clearDir 43 | write-host -foregroundColor "green" "=====> DONE" 44 | -------------------------------------------------------------------------------- /src/Bolero.TodoMVC.Client/template.html: -------------------------------------------------------------------------------- 1 |
2 |

todos

3 | 4 |
5 |
6 | 7 | 8 |
    9 | ${Entries} 10 | 20 |
21 |
22 |
23 | ${ItemsLeft} 24 | 35 | 36 |
37 | -------------------------------------------------------------------------------- /Bolero.TodoMVC.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 15 3 | VisualStudioVersion = 15.0.26124.0 4 | MinimumVisualStudioVersion = 15.0.26124.0 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{87F6C079-CBD7-4866-BDEB-CBDEA9E51D2A}" 6 | EndProject 7 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Bolero.TodoMVC.Client", "src\Bolero.TodoMVC.Client\Bolero.TodoMVC.Client.fsproj", "{1481BBD6-A6DA-4912-A03A-134880D14701}" 8 | EndProject 9 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{AA3D9964-42D1-4513-B7A7-4AC9D85FEA5A}" 10 | ProjectSection(SolutionItems) = preProject 11 | .gitignore = .gitignore 12 | tools\gh-pages.ps1 = tools\gh-pages.ps1 13 | paket.dependencies = paket.dependencies 14 | EndProjectSection 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Debug|x64 = Debug|x64 20 | Debug|x86 = Debug|x86 21 | Release|Any CPU = Release|Any CPU 22 | Release|x64 = Release|x64 23 | Release|x86 = Release|x86 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {1481BBD6-A6DA-4912-A03A-134880D14701}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {1481BBD6-A6DA-4912-A03A-134880D14701}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {1481BBD6-A6DA-4912-A03A-134880D14701}.Debug|x64.ActiveCfg = Debug|Any CPU 29 | {1481BBD6-A6DA-4912-A03A-134880D14701}.Debug|x64.Build.0 = Debug|Any CPU 30 | {1481BBD6-A6DA-4912-A03A-134880D14701}.Debug|x86.ActiveCfg = Debug|Any CPU 31 | {1481BBD6-A6DA-4912-A03A-134880D14701}.Debug|x86.Build.0 = Debug|Any CPU 32 | {1481BBD6-A6DA-4912-A03A-134880D14701}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {1481BBD6-A6DA-4912-A03A-134880D14701}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {1481BBD6-A6DA-4912-A03A-134880D14701}.Release|x64.ActiveCfg = Release|Any CPU 35 | {1481BBD6-A6DA-4912-A03A-134880D14701}.Release|x64.Build.0 = Release|Any CPU 36 | {1481BBD6-A6DA-4912-A03A-134880D14701}.Release|x86.ActiveCfg = Release|Any CPU 37 | {1481BBD6-A6DA-4912-A03A-134880D14701}.Release|x86.Build.0 = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(SolutionProperties) = preSolution 40 | HideSolutionNode = FALSE 41 | EndGlobalSection 42 | GlobalSection(NestedProjects) = preSolution 43 | {1481BBD6-A6DA-4912-A03A-134880D14701} = {87F6C079-CBD7-4866-BDEB-CBDEA9E51D2A} 44 | EndGlobalSection 45 | GlobalSection(ExtensibilityGlobals) = postSolution 46 | SolutionGuid = {8CFD6033-FB8D-48BE-9628-9D83DC023CAB} 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /src/Bolero.TodoMVC.Client/wwwroot/base.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /paket.lock: -------------------------------------------------------------------------------- 1 | STORAGE: NONE 2 | RESTRICTION: == net6.0 3 | NUGET 4 | remote: https://api.nuget.org/v3/index.json 5 | Bolero (0.20.12) 6 | Elmish (>= 3.1 < 4.0) 7 | FSharp.Core (>= 6.0) 8 | FSharp.SystemTextJson (>= 0.17.4) 9 | Microsoft.AspNetCore.Components.WebAssembly (>= 6.0) 10 | Microsoft.Extensions.Http (>= 6.0) 11 | Bolero.Build (0.20.12) 12 | Elmish (3.1) 13 | FSharp.Core (>= 4.6.2) 14 | FSharp.Core (6.0.3) 15 | FSharp.SystemTextJson (0.17.4) 16 | FSharp.Core (>= 4.7) 17 | System.Text.Json (>= 4.6) 18 | Microsoft.AspNetCore.Authorization (6.0.4) 19 | Microsoft.AspNetCore.Metadata (>= 6.0.4) 20 | Microsoft.Extensions.Logging.Abstractions (>= 6.0.1) 21 | Microsoft.Extensions.Options (>= 6.0) 22 | Microsoft.AspNetCore.Components (6.0.4) 23 | Microsoft.AspNetCore.Authorization (>= 6.0.4) 24 | Microsoft.AspNetCore.Components.Analyzers (>= 6.0.4) 25 | Microsoft.AspNetCore.Components.Analyzers (6.0.4) 26 | Microsoft.AspNetCore.Components.Forms (6.0.4) 27 | Microsoft.AspNetCore.Components (>= 6.0.4) 28 | Microsoft.AspNetCore.Components.Web (6.0.4) 29 | Microsoft.AspNetCore.Components (>= 6.0.4) 30 | Microsoft.AspNetCore.Components.Forms (>= 6.0.4) 31 | Microsoft.Extensions.DependencyInjection (>= 6.0) 32 | Microsoft.JSInterop (>= 6.0.4) 33 | System.IO.Pipelines (>= 6.0.2) 34 | Microsoft.AspNetCore.Components.WebAssembly (6.0.4) 35 | Microsoft.AspNetCore.Components.Web (>= 6.0.4) 36 | Microsoft.Extensions.Configuration.Binder (>= 6.0) 37 | Microsoft.Extensions.Configuration.Json (>= 6.0) 38 | Microsoft.Extensions.Logging (>= 6.0) 39 | Microsoft.JSInterop.WebAssembly (>= 6.0.4) 40 | Microsoft.AspNetCore.Components.WebAssembly.DevServer (6.0.4) 41 | Microsoft.AspNetCore.Metadata (6.0.4) 42 | Microsoft.Extensions.Configuration (6.0.1) 43 | Microsoft.Extensions.Configuration.Abstractions (>= 6.0) 44 | Microsoft.Extensions.Primitives (>= 6.0) 45 | Microsoft.Extensions.Configuration.Abstractions (6.0) 46 | Microsoft.Extensions.Primitives (>= 6.0) 47 | Microsoft.Extensions.Configuration.Binder (6.0) 48 | Microsoft.Extensions.Configuration.Abstractions (>= 6.0) 49 | Microsoft.Extensions.Configuration.FileExtensions (6.0) 50 | Microsoft.Extensions.Configuration (>= 6.0) 51 | Microsoft.Extensions.Configuration.Abstractions (>= 6.0) 52 | Microsoft.Extensions.FileProviders.Abstractions (>= 6.0) 53 | Microsoft.Extensions.FileProviders.Physical (>= 6.0) 54 | Microsoft.Extensions.Primitives (>= 6.0) 55 | Microsoft.Extensions.Configuration.Json (6.0) 56 | Microsoft.Extensions.Configuration (>= 6.0) 57 | Microsoft.Extensions.Configuration.Abstractions (>= 6.0) 58 | Microsoft.Extensions.Configuration.FileExtensions (>= 6.0) 59 | Microsoft.Extensions.FileProviders.Abstractions (>= 6.0) 60 | System.Text.Json (>= 6.0) 61 | Microsoft.Extensions.DependencyInjection (6.0) 62 | Microsoft.Extensions.DependencyInjection.Abstractions (>= 6.0) 63 | System.Runtime.CompilerServices.Unsafe (>= 6.0) 64 | Microsoft.Extensions.DependencyInjection.Abstractions (6.0) 65 | Microsoft.Extensions.FileProviders.Abstractions (6.0) 66 | Microsoft.Extensions.Primitives (>= 6.0) 67 | Microsoft.Extensions.FileProviders.Physical (6.0) 68 | Microsoft.Extensions.FileProviders.Abstractions (>= 6.0) 69 | Microsoft.Extensions.FileSystemGlobbing (>= 6.0) 70 | Microsoft.Extensions.Primitives (>= 6.0) 71 | Microsoft.Extensions.FileSystemGlobbing (6.0) 72 | Microsoft.Extensions.Http (6.0) 73 | Microsoft.Extensions.DependencyInjection.Abstractions (>= 6.0) 74 | Microsoft.Extensions.Logging (>= 6.0) 75 | Microsoft.Extensions.Logging.Abstractions (>= 6.0) 76 | Microsoft.Extensions.Options (>= 6.0) 77 | Microsoft.Extensions.Logging (6.0) 78 | Microsoft.Extensions.DependencyInjection (>= 6.0) 79 | Microsoft.Extensions.DependencyInjection.Abstractions (>= 6.0) 80 | Microsoft.Extensions.Logging.Abstractions (>= 6.0) 81 | Microsoft.Extensions.Options (>= 6.0) 82 | System.Diagnostics.DiagnosticSource (>= 6.0) 83 | Microsoft.Extensions.Logging.Abstractions (6.0.1) 84 | Microsoft.Extensions.Options (6.0) 85 | Microsoft.Extensions.DependencyInjection.Abstractions (>= 6.0) 86 | Microsoft.Extensions.Primitives (>= 6.0) 87 | Microsoft.Extensions.Primitives (6.0) 88 | System.Runtime.CompilerServices.Unsafe (>= 6.0) 89 | Microsoft.JSInterop (6.0.4) 90 | Microsoft.JSInterop.WebAssembly (6.0.4) 91 | Microsoft.JSInterop (>= 6.0.4) 92 | System.Diagnostics.DiagnosticSource (6.0) 93 | System.Runtime.CompilerServices.Unsafe (>= 6.0) 94 | System.IO.Pipelines (6.0.2) 95 | System.Runtime.CompilerServices.Unsafe (6.0) 96 | System.Text.Encodings.Web (6.0) 97 | System.Runtime.CompilerServices.Unsafe (>= 6.0) 98 | System.Text.Json (6.0.3) 99 | System.Runtime.CompilerServices.Unsafe (>= 6.0) 100 | System.Text.Encodings.Web (>= 6.0) 101 | -------------------------------------------------------------------------------- /src/Bolero.TodoMVC.Client/Main.fs: -------------------------------------------------------------------------------- 1 | module Bolero.TodoMVC.Client.Main 2 | 3 | open Elmish 4 | open Bolero 5 | open Bolero.Html 6 | 7 | /// Parses the template.html file and provides types to fill it with dynamic content. 8 | type MasterTemplate = Template<"template.html"> 9 | 10 | /// Our application has three URL endpoints. 11 | type EndPoint = 12 | | [] All 13 | | [] Active 14 | | [] Completed 15 | 16 | /// This module defines the model, the update and the view for a single entry. 17 | module Entry = 18 | 19 | /// The unique identifier of a Todo entry. 20 | type Key = int 21 | 22 | /// The model for a Todo entry. 23 | type Model = 24 | { 25 | Id : Key 26 | Task : string 27 | IsCompleted : bool 28 | Editing : option 29 | } 30 | 31 | let New (key: Key) (task: string) = 32 | { 33 | Id = key 34 | Task = task 35 | IsCompleted = false 36 | Editing = None 37 | } 38 | 39 | type Message = 40 | | Remove 41 | | StartEdit 42 | | Edit of text: string 43 | | CommitEdit 44 | | CancelEdit 45 | | SetCompleted of completed: bool 46 | 47 | /// Defines how a given Todo entry is updated based on a message. 48 | /// Returns Some to update the entry, or None to delete it. 49 | let Update (msg: Message) (e: Model) : option = 50 | match msg with 51 | | Remove -> 52 | None 53 | | StartEdit -> 54 | Some { e with Editing = Some e.Task } 55 | | Edit value -> 56 | Some { e with Editing = e.Editing |> Option.map (fun _ -> value) } 57 | | CommitEdit -> 58 | Some { e with 59 | Task = e.Editing |> Option.defaultValue e.Task 60 | Editing = None } 61 | | CancelEdit -> 62 | Some { e with Editing = None } 63 | | SetCompleted value -> 64 | Some { e with IsCompleted = value } 65 | 66 | /// Render a given Todo entry. 67 | let Render (endpoint, entry) dispatch = 68 | MasterTemplate.Entry() 69 | .Label(text entry.Task) 70 | .CssAttrs( 71 | attr.``class`` (String.concat " " [ 72 | if entry.IsCompleted then "completed" 73 | if entry.Editing.IsSome then "editing" 74 | match endpoint, entry.IsCompleted with 75 | | EndPoint.Completed, false 76 | | EndPoint.Active, true -> "hidden" 77 | | _ -> () 78 | ]) 79 | ) 80 | .EditingTask( 81 | entry.Editing |> Option.defaultValue "", 82 | fun text -> dispatch (Message.Edit text) 83 | ) 84 | .EditBlur(fun _ -> dispatch Message.CommitEdit) 85 | .EditKeyup(fun e -> 86 | match e.Key with 87 | | "Enter" -> dispatch Message.CommitEdit 88 | | "Escape" -> dispatch Message.CancelEdit 89 | | _ -> () 90 | ) 91 | .IsCompleted( 92 | entry.IsCompleted, 93 | fun x -> dispatch (Message.SetCompleted x) 94 | ) 95 | .Remove(fun _ -> dispatch Message.Remove) 96 | .StartEdit(fun _ -> dispatch Message.StartEdit) 97 | .Elt() 98 | 99 | type Component() = 100 | inherit ElmishComponent() 101 | 102 | override this.ShouldRender(oldModel, newModel) = oldModel <> newModel 103 | 104 | override this.View model dispatch = Render model dispatch 105 | 106 | /// This module defines the model, the update and the view for a full todo list. 107 | module TodoList = 108 | 109 | /// The model for the full TodoList application. 110 | type Model = 111 | { 112 | EndPoint : EndPoint 113 | NewTask : string 114 | Entries : list 115 | NextKey : Entry.Key 116 | } 117 | 118 | static member Empty = 119 | { 120 | EndPoint = All 121 | NewTask = "" 122 | Entries = [] 123 | NextKey = 0 124 | } 125 | 126 | type Message = 127 | | EditNewTask of text: string 128 | | AddEntry 129 | | ClearCompleted 130 | | SetAllCompleted of completed: bool 131 | | EntryMessage of key: Entry.Key * message: Entry.Message 132 | | SetEndPoint of EndPoint 133 | 134 | let Router = Router.infer SetEndPoint (fun m -> m.EndPoint) 135 | 136 | /// Defines how the Todo list is updated based on a message. 137 | let Update (msg: Message) (model: Model) = 138 | match msg with 139 | | EditNewTask value -> 140 | { model with NewTask = value } 141 | | AddEntry -> 142 | { model with 143 | NewTask = "" 144 | Entries = model.Entries @ [Entry.New model.NextKey model.NewTask] 145 | NextKey = model.NextKey + 1 } 146 | | ClearCompleted -> 147 | { model with Entries = List.filter (fun e -> not e.IsCompleted) model.Entries } 148 | | SetAllCompleted c -> 149 | { model with Entries = List.map (fun e -> { e with IsCompleted = c }) model.Entries } 150 | | EntryMessage (key, msg) -> 151 | let updateEntry (e: Entry.Model) = 152 | if e.Id = key then Entry.Update msg e else Some e 153 | { model with Entries = List.choose updateEntry model.Entries } 154 | | SetEndPoint ep -> 155 | { model with EndPoint = ep } 156 | 157 | /// Render the whole application. 158 | let Render (state: Model) (dispatch: Dispatch) = 159 | let countNotCompleted = 160 | state.Entries 161 | |> List.filter (fun e -> not e.IsCompleted) 162 | |> List.length 163 | MasterTemplate() 164 | .HiddenIfNoEntries(if List.isEmpty state.Entries then "hidden" else "") 165 | .Entries(concat { 166 | for entry in state.Entries do 167 | let entryDispatch msg = dispatch (EntryMessage (entry.Id, msg)) 168 | ecomp (state.EndPoint, entry) entryDispatch 169 | }) 170 | .ClearCompleted(fun _ -> dispatch Message.ClearCompleted) 171 | .IsCompleted( 172 | (countNotCompleted = 0), 173 | fun c -> dispatch (Message.SetAllCompleted c) 174 | ) 175 | .Task( 176 | state.NewTask, 177 | fun text -> dispatch (Message.EditNewTask text) 178 | ) 179 | .Edit(fun e -> 180 | if e.Key = "Enter" && state.NewTask <> "" then 181 | dispatch Message.AddEntry 182 | ) 183 | .ItemsLeft( 184 | match countNotCompleted with 185 | | 1 -> "1 item left" 186 | | n -> string n + " items left" 187 | ) 188 | .CssFilterAll(attr.``class`` (if state.EndPoint = EndPoint.All then "selected" else null)) 189 | .CssFilterActive(attr.``class`` (if state.EndPoint = EndPoint.Active then "selected" else null)) 190 | .CssFilterCompleted(attr.``class`` (if state.EndPoint = EndPoint.Completed then "selected" else null)) 191 | .Elt() 192 | 193 | /// The entry point of our application, called on page load (see Startup.fs). 194 | type Component() = 195 | inherit ProgramComponent() 196 | 197 | override this.Program = 198 | Program.mkSimple (fun _ -> Model.Empty) Update Render 199 | |> Program.withRouter Router 200 | -------------------------------------------------------------------------------- /src/Bolero.TodoMVC.Client/wwwroot/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | } 22 | 23 | body { 24 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 25 | line-height: 1.4em; 26 | background: #f5f5f5; 27 | color: #4d4d4d; 28 | min-width: 230px; 29 | max-width: 550px; 30 | margin: 0 auto; 31 | -webkit-font-smoothing: antialiased; 32 | -moz-osx-font-smoothing: grayscale; 33 | font-weight: 300; 34 | } 35 | 36 | :focus { 37 | outline: 0; 38 | } 39 | 40 | .hidden { 41 | display: none; 42 | } 43 | 44 | .todoapp { 45 | background: #fff; 46 | margin: 130px 0 40px 0; 47 | position: relative; 48 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 49 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 50 | } 51 | 52 | .todoapp input::-webkit-input-placeholder { 53 | font-style: italic; 54 | font-weight: 300; 55 | color: #e6e6e6; 56 | } 57 | 58 | .todoapp input::-moz-placeholder { 59 | font-style: italic; 60 | font-weight: 300; 61 | color: #e6e6e6; 62 | } 63 | 64 | .todoapp input::input-placeholder { 65 | font-style: italic; 66 | font-weight: 300; 67 | color: #e6e6e6; 68 | } 69 | 70 | .todoapp h1 { 71 | position: absolute; 72 | top: -155px; 73 | width: 100%; 74 | font-size: 100px; 75 | font-weight: 100; 76 | text-align: center; 77 | color: rgba(175, 47, 47, 0.15); 78 | -webkit-text-rendering: optimizeLegibility; 79 | -moz-text-rendering: optimizeLegibility; 80 | text-rendering: optimizeLegibility; 81 | } 82 | 83 | .new-todo, 84 | .edit { 85 | position: relative; 86 | margin: 0; 87 | width: 100%; 88 | font-size: 24px; 89 | font-family: inherit; 90 | font-weight: inherit; 91 | line-height: 1.4em; 92 | border: 0; 93 | color: inherit; 94 | padding: 6px; 95 | border: 1px solid #999; 96 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 97 | box-sizing: border-box; 98 | -webkit-font-smoothing: antialiased; 99 | -moz-osx-font-smoothing: grayscale; 100 | } 101 | 102 | .new-todo { 103 | padding: 16px 16px 16px 60px; 104 | border: none; 105 | background: rgba(0, 0, 0, 0.003); 106 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 107 | } 108 | 109 | .main { 110 | position: relative; 111 | z-index: 2; 112 | border-top: 1px solid #e6e6e6; 113 | } 114 | 115 | .toggle-all { 116 | width: 1px; 117 | height: 1px; 118 | border: none; /* Mobile Safari */ 119 | opacity: 0; 120 | position: absolute; 121 | right: 100%; 122 | bottom: 100%; 123 | } 124 | 125 | .toggle-all + label { 126 | width: 60px; 127 | height: 34px; 128 | font-size: 0; 129 | position: absolute; 130 | top: -52px; 131 | left: -13px; 132 | -webkit-transform: rotate(90deg); 133 | transform: rotate(90deg); 134 | } 135 | 136 | .toggle-all + label:before { 137 | content: '❯'; 138 | font-size: 22px; 139 | color: #e6e6e6; 140 | padding: 10px 27px 10px 27px; 141 | } 142 | 143 | .toggle-all:checked + label:before { 144 | color: #737373; 145 | } 146 | 147 | .todo-list { 148 | margin: 0; 149 | padding: 0; 150 | list-style: none; 151 | } 152 | 153 | .todo-list li { 154 | position: relative; 155 | font-size: 24px; 156 | border-bottom: 1px solid #ededed; 157 | } 158 | 159 | .todo-list li:last-child { 160 | border-bottom: none; 161 | } 162 | 163 | .todo-list li.editing { 164 | border-bottom: none; 165 | padding: 0; 166 | } 167 | 168 | .todo-list li.editing .edit { 169 | display: block; 170 | width: 506px; 171 | padding: 12px 16px; 172 | margin: 0 0 0 43px; 173 | } 174 | 175 | .todo-list li.editing .view { 176 | display: none; 177 | } 178 | 179 | .todo-list li .toggle { 180 | text-align: center; 181 | width: 40px; 182 | /* auto, since non-WebKit browsers doesn't support input styling */ 183 | height: auto; 184 | position: absolute; 185 | top: 0; 186 | bottom: 0; 187 | margin: auto 0; 188 | border: none; /* Mobile Safari */ 189 | -webkit-appearance: none; 190 | appearance: none; 191 | } 192 | 193 | .todo-list li .toggle { 194 | opacity: 0; 195 | } 196 | 197 | .todo-list li .toggle + label { 198 | /* 199 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 200 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ 201 | */ 202 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); 203 | background-repeat: no-repeat; 204 | background-position: center left; 205 | } 206 | 207 | .todo-list li .toggle:checked + label { 208 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); 209 | } 210 | 211 | .todo-list li label { 212 | word-break: break-all; 213 | padding: 15px 15px 15px 60px; 214 | display: block; 215 | line-height: 1.2; 216 | transition: color 0.4s; 217 | } 218 | 219 | .todo-list li.completed label { 220 | color: #d9d9d9; 221 | text-decoration: line-through; 222 | } 223 | 224 | .todo-list li .destroy { 225 | display: none; 226 | position: absolute; 227 | top: 0; 228 | right: 10px; 229 | bottom: 0; 230 | width: 40px; 231 | height: 40px; 232 | margin: auto 0; 233 | font-size: 30px; 234 | color: #cc9a9a; 235 | margin-bottom: 11px; 236 | transition: color 0.2s ease-out; 237 | } 238 | 239 | .todo-list li .destroy:hover { 240 | color: #af5b5e; 241 | } 242 | 243 | .todo-list li .destroy:after { 244 | content: '×'; 245 | } 246 | 247 | .todo-list li:hover .destroy { 248 | display: block; 249 | } 250 | 251 | .todo-list li .edit { 252 | display: none; 253 | } 254 | 255 | .todo-list li.editing:last-child { 256 | margin-bottom: -1px; 257 | } 258 | 259 | .footer { 260 | color: #777; 261 | padding: 10px 15px; 262 | height: 20px; 263 | text-align: center; 264 | border-top: 1px solid #e6e6e6; 265 | } 266 | 267 | .footer:before { 268 | content: ''; 269 | position: absolute; 270 | right: 0; 271 | bottom: 0; 272 | left: 0; 273 | height: 50px; 274 | overflow: hidden; 275 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 276 | 0 8px 0 -3px #f6f6f6, 277 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 278 | 0 16px 0 -6px #f6f6f6, 279 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 280 | } 281 | 282 | .todo-count { 283 | float: left; 284 | text-align: left; 285 | } 286 | 287 | .todo-count strong { 288 | font-weight: 300; 289 | } 290 | 291 | .filters { 292 | margin: 0; 293 | padding: 0; 294 | list-style: none; 295 | position: absolute; 296 | right: 0; 297 | left: 0; 298 | } 299 | 300 | .filters li { 301 | display: inline; 302 | } 303 | 304 | .filters li a { 305 | color: inherit; 306 | margin: 3px; 307 | padding: 3px 7px; 308 | text-decoration: none; 309 | border: 1px solid transparent; 310 | border-radius: 3px; 311 | } 312 | 313 | .filters li a:hover { 314 | border-color: rgba(175, 47, 47, 0.1); 315 | } 316 | 317 | .filters li a.selected { 318 | border-color: rgba(175, 47, 47, 0.2); 319 | } 320 | 321 | .clear-completed, 322 | html .clear-completed:active { 323 | float: right; 324 | position: relative; 325 | line-height: 20px; 326 | text-decoration: none; 327 | cursor: pointer; 328 | } 329 | 330 | .clear-completed:hover { 331 | text-decoration: underline; 332 | } 333 | 334 | .info { 335 | margin: 65px auto 0; 336 | color: #bfbfbf; 337 | font-size: 10px; 338 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 339 | text-align: center; 340 | } 341 | 342 | .info p { 343 | line-height: 1; 344 | } 345 | 346 | .info a { 347 | color: inherit; 348 | text-decoration: none; 349 | font-weight: 400; 350 | } 351 | 352 | .info a:hover { 353 | text-decoration: underline; 354 | } 355 | 356 | /* 357 | Hack to remove background from Mobile Safari. 358 | Can't use it globally since it destroys checkboxes in Firefox 359 | */ 360 | @media screen and (-webkit-min-device-pixel-ratio:0) { 361 | .toggle-all, 362 | .todo-list li .toggle { 363 | background: none; 364 | } 365 | 366 | .todo-list li .toggle { 367 | height: 40px; 368 | } 369 | } 370 | 371 | @media (max-width: 430px) { 372 | .footer { 373 | height: 50px; 374 | } 375 | 376 | .filters { 377 | bottom: 10px; 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /.paket/Paket.Restore.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 8 | 9 | $(MSBuildVersion) 10 | 15.0.0 11 | false 12 | true 13 | 14 | true 15 | $(MSBuildThisFileDirectory) 16 | $(MSBuildThisFileDirectory)..\ 17 | $(PaketRootPath)paket-files\paket.restore.cached 18 | $(PaketRootPath)paket.lock 19 | classic 20 | proj 21 | assembly 22 | native 23 | /Library/Frameworks/Mono.framework/Commands/mono 24 | mono 25 | 26 | 27 | $(PaketRootPath)paket.bootstrapper.exe 28 | $(PaketToolsPath)paket.bootstrapper.exe 29 | $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\ 30 | 31 | "$(PaketBootStrapperExePath)" 32 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" 33 | 34 | 35 | 36 | 37 | true 38 | true 39 | 40 | 41 | True 42 | 43 | 44 | False 45 | 46 | $(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/')) 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | $(PaketRootPath)paket 56 | $(PaketToolsPath)paket 57 | 58 | 59 | 60 | 61 | 62 | $(PaketRootPath)paket.exe 63 | $(PaketToolsPath)paket.exe 64 | 65 | 66 | 67 | 68 | 69 | <_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json")) 70 | <_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"')) 71 | <_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | <_PaketCommand>dotnet paket 83 | 84 | 85 | 86 | 87 | 88 | $(PaketToolsPath)paket 89 | $(PaketBootStrapperExeDir)paket 90 | 91 | 92 | paket 93 | 94 | 95 | 96 | 97 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) 98 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)" 99 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 100 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)" 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | true 122 | $(NoWarn);NU1603;NU1604;NU1605;NU1608 123 | false 124 | true 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) 134 | 135 | 136 | 137 | 138 | 139 | 141 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[0].Replace(`"`, ``).Replace(` `, ``)) 142 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[1].Replace(`"`, ``).Replace(` `, ``)) 143 | 144 | 145 | 146 | 147 | %(PaketRestoreCachedKeyValue.Value) 148 | %(PaketRestoreCachedKeyValue.Value) 149 | 150 | 151 | 152 | 153 | true 154 | false 155 | true 156 | 157 | 158 | 162 | 163 | true 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | $(PaketIntermediateOutputPath)\$(MSBuildProjectFile).paket.references.cached 183 | 184 | $(MSBuildProjectFullPath).paket.references 185 | 186 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 187 | 188 | $(MSBuildProjectDirectory)\paket.references 189 | 190 | false 191 | true 192 | true 193 | references-file-or-cache-not-found 194 | 195 | 196 | 197 | 198 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) 199 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) 200 | references-file 201 | false 202 | 203 | 204 | 205 | 206 | false 207 | 208 | 209 | 210 | 211 | true 212 | target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths) 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | false 224 | true 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length) 236 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) 237 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) 238 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) 239 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5]) 240 | 241 | 242 | %(PaketReferencesFileLinesInfo.PackageVersion) 243 | All 244 | runtime 245 | runtime 246 | true 247 | true 248 | 249 | 250 | 251 | 252 | $(PaketIntermediateOutputPath)/$(MSBuildProjectFile).paket.clitools 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) 262 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) 263 | 264 | 265 | %(PaketCliToolFileLinesInfo.PackageVersion) 266 | 267 | 268 | 269 | 273 | 274 | 275 | 276 | 277 | 278 | false 279 | 280 | 281 | 282 | 283 | 284 | <_NuspecFilesNewLocation Include="$(PaketIntermediateOutputPath)\$(Configuration)\*.nuspec"/> 285 | 286 | 287 | 288 | 289 | 290 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile) 291 | true 292 | false 293 | true 294 | false 295 | true 296 | false 297 | true 298 | false 299 | true 300 | $(PaketIntermediateOutputPath)\$(Configuration) 301 | $(PaketIntermediateOutputPath) 302 | 303 | 304 | 305 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.$(PackageVersion.Split(`+`)[0]).nuspec"/> 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 363 | 364 | 407 | 408 | 450 | 451 | 492 | 493 | 494 | 495 | --------------------------------------------------------------------------------