Skip to content

Commit

Permalink
Merge branch 'htmx' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
Lanayx committed Feb 29, 2024
2 parents c1d8db6 + dc7fe99 commit ae20eb7
Show file tree
Hide file tree
Showing 24 changed files with 1,006 additions and 0 deletions.
1 change: 1 addition & 0 deletions .fantomasignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#ignore preconditional tests with long arrays
tests/Oxpecker.Tests/Preconditional.Tests.fs
tests/Oxpecker.Tests/Streaming.Tests.fs
examples/ContactApp
14 changes: 14 additions & 0 deletions Oxpecker.sln
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "PerfTest", "tests\PerfTest\
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Basic", "examples\Basic\Basic.fsproj", "{B6DBDACA-E694-4B9F-95A0-0902C7B21555}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Oxpecker.Htmx", "src\Oxpecker.Htmx\Oxpecker.Htmx.fsproj", "{48D48FCE-2530-4DD4-B9A7-437E9F4886F1}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ContactApp", "examples\ContactApp\ContactApp.fsproj", "{EA28ADEF-63B9-4E17-94E4-7894848D44E2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -54,6 +58,14 @@ Global
{B6DBDACA-E694-4B9F-95A0-0902C7B21555}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B6DBDACA-E694-4B9F-95A0-0902C7B21555}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B6DBDACA-E694-4B9F-95A0-0902C7B21555}.Release|Any CPU.Build.0 = Release|Any CPU
{48D48FCE-2530-4DD4-B9A7-437E9F4886F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{48D48FCE-2530-4DD4-B9A7-437E9F4886F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{48D48FCE-2530-4DD4-B9A7-437E9F4886F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{48D48FCE-2530-4DD4-B9A7-437E9F4886F1}.Release|Any CPU.Build.0 = Release|Any CPU
{EA28ADEF-63B9-4E17-94E4-7894848D44E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EA28ADEF-63B9-4E17-94E4-7894848D44E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA28ADEF-63B9-4E17-94E4-7894848D44E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA28ADEF-63B9-4E17-94E4-7894848D44E2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{625D9E19-8077-40D5-B208-23AFAC2B5F75} = {9EDF9D3C-DA11-44A2-A20C-4B466FEB7E06}
Expand All @@ -63,5 +75,7 @@ Global
{0FD9D105-2EE3-4BB0-B7A3-86084040F925} = {40C4900E-D46D-450F-9A93-A2CE2E4EF5CA}
{E0F1FCEC-CAA2-4FCA-9452-D8551F188B88} = {40C4900E-D46D-450F-9A93-A2CE2E4EF5CA}
{B6DBDACA-E694-4B9F-95A0-0902C7B21555} = {A1BC9EB1-C6D9-470C-B0F6-323814A0D0AC}
{48D48FCE-2530-4DD4-B9A7-437E9F4886F1} = {9EDF9D3C-DA11-44A2-A20C-4B466FEB7E06}
{EA28ADEF-63B9-4E17-94E4-7894848D44E2} = {A1BC9EB1-C6D9-470C-B0F6-323814A0D0AC}
EndGlobalSection
EndGlobal
1 change: 1 addition & 0 deletions examples/Basic/Basic.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<Compile Include="Program.fs"/>
<Content Include="README.md" />
</ItemGroup>

<ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions examples/Basic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Basic examples

Here you can find a dump of different route patterns and handlers all in one file.
1 change: 1 addition & 0 deletions examples/CRUD/CRUD.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<Compile Include="Env.fs" />
<Compile Include="Handlers.fs" />
<Compile Include="Program.fs"/>
<Content Include="README.md" />
</ItemGroup>

<ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions examples/CRUD/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## CRUD example

Here you can find a JSON api for basic CRUD operations. [Functional DI](https://www.bartoszsypytkowski.com/dealing-with-complex-dependency-injection-in-f/) is used to glue components together.
34 changes: 34 additions & 0 deletions examples/ContactApp/ContactApp.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Content Include="wwwroot\site.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="wwwroot\spinning-circles.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Compile Include="Models.fs" />
<Compile Include="Tools.fs" />
<Compile Include="templates\shared\layout.fs" />
<Compile Include="templates\shared\contactFields.fs" />
<Compile Include="templates\index.fs" />
<Compile Include="templates\show.fs" />
<Compile Include="templates\edit.fs" />
<Compile Include="templates\new.fs" />
<Compile Include="ContactService.fs" />
<Compile Include="Handlers.fs" />
<Compile Include="Program.fs" />
<Content Include="README.md" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Oxpecker.Htmx\Oxpecker.Htmx.fsproj" />
<ProjectReference Include="..\..\src\Oxpecker\Oxpecker.fsproj" />
</ItemGroup>

</Project>
54 changes: 54 additions & 0 deletions examples/ContactApp/ContactService.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
module ContactApp.ContactService

open System
open System.Threading
open ContactApp.Models

let internal contactDb = ResizeArray([
{ Id = 1; First = "John"; Last = "Smith"; Email = "john@example.com"; Phone = "123-456-7890" }
{ Id = 2; First = "Dana"; Last = "Crandith"; Email = "dcran@example.com"; Phone = "123-456-7890" }
{ Id = 3; First = "Edith"; Last = "Neutvaar"; Email = "en@example.com"; Phone = "123-456-7890" }
{ Id = 4; First = "John2"; Last = "Smith"; Email = "john2@example.com"; Phone = "123-456-7890" }
{ Id = 5; First = "Dana2"; Last = "Crandith"; Email = "dcran2@example.com"; Phone = "123-456-7890" }
{ Id = 6; First = "Edith2"; Last = "Neutvaar"; Email = "en2@example.com"; Phone = "123-456-7890" }
{ Id = 7; First = "John3"; Last = "Smith"; Email = "john3@example.com"; Phone = "123-456-7890" }
{ Id = 8; First = "Dana3"; Last = "Crandith"; Email = "dcran3@example.com"; Phone = "123-456-7890" }
{ Id = 9; First = "Edith3"; Last = "Neutvaar"; Email = "en3@example.com"; Phone = "123-456-7890" }
{ Id = 10; First = "John4"; Last = "Smith"; Email = "john4@example.com"; Phone = "123-456-7890" }
{ Id = 11; First = "Dana4"; Last = "Crandith"; Email = "dcran4@example.com"; Phone = "123-456-7890" }
{ Id = 12; First = "Edith4"; Last = "Neutvaar"; Email = "en4@example.com"; Phone = "123-456-7890" }
])

let count() =
Thread.Sleep 2000
contactDb.Count

let searchContact (search: string) =
contactDb
|> Seq.filter(fun c -> c.First.Contains(search, StringComparison.OrdinalIgnoreCase)
|| c.Last.Contains(search, StringComparison.OrdinalIgnoreCase))

let all page =
contactDb |> Seq.skip ((page-1)*5) |> Seq.truncate 5

let add (contact: Contact) =
let newId = contactDb |> Seq.maxBy(fun c -> c.Id) |> fun c -> c.Id + 1
let newContact = { contact with Id = newId }
contactDb.Add(newContact)
newContact

let find id =
contactDb.Find(fun c -> c.Id = id)

let update (contact: Contact) =
let index = contactDb.FindIndex(fun c -> c.Id = contact.Id)
contactDb[index] <- contact

let delete id =
contactDb.RemoveAll(fun c -> c.Id = id)

let validateEmail (contact: Contact) =
let existingContact = contactDb |> Seq.tryFind(fun c -> c.Email = contact.Email)
match existingContact with
| Some c when c.Id <> contact.Id -> false
| _ -> true
140 changes: 140 additions & 0 deletions examples/ContactApp/Handlers.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
module ContactApp.Handlers
open System.Threading.Tasks
open ContactApp.templates
open ContactApp.Models
open ContactApp.Tools
open Microsoft.AspNetCore.Http
open Oxpecker

let mutable archiver = Archiver(ResizeArray())

let getContacts: EndpointHandler =
fun ctx ->
let page = ctx.TryGetQueryValue "page" |> Option.map int |> Option.defaultValue 1
match ctx.TryGetQueryValue "q" with
| Some search ->
let result =
ContactService.searchContact search
|> Seq.toArray
match ctx.TryGetHeaderValue "HX-Trigger" with
| Some "search" ->
ctx.WriteHtmlView (index.rows page result)
| _ ->
ctx |> writeHtml (index.html search page result archiver)
| None ->
let result =
ContactService.all page
|> Seq.toArray
ctx |> writeHtml (index.html "" page result archiver)

let getContactsCount: EndpointHandler =
fun ctx ->
let count = ContactService.count()
ctx.WriteText $"({count} total Contacts)"

let getNewContact: EndpointHandler =
let newContact = {
id = 0
first = ""
last = ""
email = ""
phone = ""
errors = dict []
}
writeHtml (new'.html newContact)

let insertContact: EndpointHandler =
fun ctx ->
task {
let! contact = ctx.BindForm<ContactDTO>()
let validatedContact = contact.Validate()
if validatedContact.errors.Count > 0 then
return! ctx |> writeHtml (new'.html validatedContact)
else
validatedContact.ToDomain()
|> ContactService.add
|> ignore
flash "Created new Contact!" ctx
return ctx.Response.Redirect("/contacts")
}

let updateContact id: EndpointHandler =
fun ctx ->
task {
let! contact = ctx.BindForm<ContactDTO>()
let validatedContact = contact.Validate()
if validatedContact.errors.Count > 0 then
return! ctx |> writeHtml (edit.html { validatedContact with id = id })
else
let domainContact = validatedContact.ToDomain()
ContactService.update({domainContact with Id = id})
flash "Updated Contact!" ctx
return ctx.Response.Redirect($"/contacts/{id}")
}

let viewContact id: EndpointHandler =
let contact = ContactService.find id
writeHtml <| show.html contact

let getEditContact id: EndpointHandler =
let contact = ContactService.find id |> ContactDTO.FromDomain
writeHtml <| edit.html contact

let deleteContact id: EndpointHandler =
fun ctx ->
task {
ContactService.delete id |> ignore
match ctx.TryGetHeaderValue "HX-Trigger" with
| Some "delete-btn" ->
flash "Deleted Contact!" ctx
ctx.Response.Redirect("/contacts")
ctx.SetStatusCode(303)
| _ ->
()
}

let deleteContacts (ctx: HttpContext) =
match ctx.TryGetFormValues "selected_contact_ids" with
| Some ids ->
for id in ids do
id |> int |> ContactService.delete |> ignore
flash "Deleted Contacts!" ctx
| None ->
()
let page = 1
let result =
ContactService.all page
|> Seq.toArray
ctx |> writeHtml (index.html "" page result archiver)

let validateEmail id: EndpointHandler =
fun ctx ->
match ctx.TryGetQueryValue("email") with
| Some email ->
let contact =
if id = 0 then
{ Id = 0; First = ""; Last = ""; Phone = ""; Email = email }
else
let contact = ContactService.find id
{ contact with Email = email }
if ContactService.validateEmail { contact with Email = email } then
Task.CompletedTask
else
ctx.WriteText "Invalid email"
| None ->
Task.CompletedTask

let startArchive: EndpointHandler =
fun ctx ->
archiver <- Archiver(ContactService.contactDb)
archiver.Run() |> ignore
ctx.WriteHtmlView (index.archiveUi archiver)

let getArchiveStatus: EndpointHandler =
fun ctx ->
ctx.WriteHtmlView (index.archiveUi archiver)

let deleteArchive: EndpointHandler =
fun ctx ->
archiver.Reset()
ctx.WriteHtmlView (index.archiveUi archiver)
37 changes: 37 additions & 0 deletions examples/ContactApp/Models.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module ContactApp.Models
open System
open System.Collections.Generic


type Contact = {
Id: int
First: string
Last: string
Phone: string
Email: string
}

[<CLIMutable>]
type ContactDTO = {
id: int
first: string
last: string
phone: string
email: string
errors: IDictionary<string, string>
} with
member this.GetError key =
match this.errors.TryGetValue key with
| true, value -> value
| _ -> ""
member this.Validate() =
let errors = Dictionary<string, string>()
if String.IsNullOrEmpty(this.first) then errors.Add("first", "First name is required")
if String.IsNullOrEmpty(this.last) then errors.Add("last", "Last name is required")
if String.IsNullOrEmpty(this.phone) then errors.Add("phone", "Phone is required")
if String.IsNullOrEmpty(this.email) then errors.Add("email", "Email is required")
{ this with errors = errors }
member this.ToDomain() =
{ Id = this.id; First = this.first; Last = this.last; Phone = this.phone; Email = this.email }
static member FromDomain(contact: Contact) =
{ id = contact.Id; first = contact.First; last = contact.Last; phone = contact.Phone; email = contact.Email; errors = dict [] }
Loading

0 comments on commit ae20eb7

Please sign in to comment.