giraffe-fsharp / Giraffe

A native functional ASP.NET Core web framework for F# developers.
https://giraffe.wiki
Apache License 2.0
2.11k stars 267 forks source link

Creating a Form Module using XmlViewEngine #126

Closed vtquan closed 5 years ago

vtquan commented 6 years ago

I have been trying to create something similar to this but using Giraffe's XmlViewEngine instead.

You can see how I port it over below but I am not satisfy with it for a few reasons

There is no validation since there is no Giraffe equivalent to Suave.Form. Of course, in Razor, we can use data annotation but not for XmlViewEngine. Creating types as in type driven development might work also but I don't know how it would work with BindForm()

I have to manually set the name of the input field. The tutorial was using Code Quotations for that but I wasn't able to get it to work.

Also I can't do a redirect in let createAlbum =. I can't close the task without the return! and that would skip my redirectTo statement.

I want to know how you would improve on this or would you approach this differently?

type Field<'a> = {
    Label : string
    Html : XmlNode
}

type Fieldset<'a> = {
    Legend : string
    Fields : Field<'a> list
}

type FormLayout<'a> = {
    Fieldsets : Fieldset<'a> list
    SubmitText : string
}

let renderForm (layout : FormLayout<_>) =
    form [
        for set in layout.Fieldsets ->
            tag "fieldset" [] [
                yield tag "legend" [] [ encodedText set.Legend ]

                for field in set.Fields do
                    yield div [ attr "class" "editor-label" ] [
                        encodedText field.Label
                    ]
                    yield div [ attr "class" "editor-field" ] [
                        field.Html
                    ]
            ]

        yield submitInput layout.SubmitText
    ]

let createAlbum (genres:Db.Genre list) (artists:Db.Artist list) = [
    h2 "Create"

    renderForm {
        Fieldsets = 
            [
                {
                    Legend = "Album"
                    Fields = 
                        [
                            {
                                Label = "Genre"
                                Html = 
                                    select [attr "name" "GenreId"] [
                                        for g in genres ->
                                            option [ attr "value" (g.Genreid.ToString())] [encodedText g.Name]
                                    ]
                            }
                            {
                                Label = "Artist"
                                Html = 
                                    select [attr "name" "ArtistId"] [
                                        for a in artists ->
                                            option [ attr "value" (a.Artistid.ToString())] [encodedText a.Name]
                                    ]
                            }
                            {
                                Label = "Title"
                                Html = 
                                    input [ attr "name" "Title"] 
                            }

                            {
                                Label = "Price"
                                Html = 
                                    input [ attr "name" "Price"] 
                            }
                            {
                                Label = "Album Art Url"
                                Html = 
                                    input [ attr "name" "ArtUrl" ]
                            }
                        ]
                }
            ]
        SubmitText = "Create"
    }

    div [] [
        a [ attr "href" Path.Admin.manage] [ encodedText "Back to list" ]
    ]
]
let createAlbum =
    let dbCtx = Db.getContext()
    choose [
        GET >=> warbler (fun _ ->
            let genres = Db.getGenres dbCtx
            let artists = Db.getArtists dbCtx
            html (View.createAlbum genres artists))
        POST >=> 
            fun (next : HttpFunc) (ctx : HttpContext) ->
                task {
                    let! album = ctx.BindForm<Album>()
                    Db.createAlbum
                        (
                            int album.ArtistId,
                            int album.GenreId,
                            album.Price,
                            album.Title
                        ) dbCtx 
                    return! next ctx
                }
            redirectTo false Path.Admin.manage
    ]
nojaf commented 6 years ago

For the redirect part, are you not missing an >=>?

let stuff =
    fun next ctx ->
        task {
            printfn "doing stuff"
            return! next ctx
        }

let webApp =
    choose [
        GET >=>
            choose [
                route "/" >=> homePage
                route "/meh" >=> stuff >=> redirectTo false "/"
            ]
        setStatusCode 404 >=> text "Not Found" ]
vtquan commented 6 years ago

Yes, that was the problem with redirect. Thank you.

dustinmoris commented 6 years ago

@vtquan Hi, I'll try to have a look at your example over the course of the next couple days and get back to you!

vtquan commented 6 years ago

@dustinmoris Thanks! If it helps, you can look at my repo to see it in a running project.

Banashek commented 6 years ago

I've been using _name "PropertyName" in my code, but I do see a possible elegant abstraction using quotations.

You could get the property names by doing something like:

let m = // Instance of your DTO model
match <@@ m @@> with | PropertyGet(_,pi,_) -> pi.Name | _ -> ""

Obviously that's just a quick fsi result, but you could go based off of that. Not sure how to do it with non-static types, but I'm sure there's a good way to do it if you keep digging.

Or you could instantiate the form with an empty instance of your model (kinda like how rails always passes models to the views, whether they're new or being updated)