Create album

We can delete an album, so why don't we proceed to add album functionality now. It will require a bit more effort, because we actually need some kind of a form with fields to create a new album. Fortunately, there's a helper module in Suave library exactly for this purpose.

Note: Suave.Form module at the time of writing is still in Experimental package - just as Suave.Html which we're already using.

First, let's create a separate module Form to keep all of our forms in there (yes there will be more soon). Add the Form.fs file just before View.fs - both View and App module will depend on Form. As with the rest of modules, don't forget to follow our modules naming convention.

Now declare the first Album form:

open Suave.Form type Album = { ArtistId : decimal GenreId : decimal Title : string Price : decimal ArtUrl : string } let album : Form<Album> = Form ([ TextProp ((fun f -> <@ f.Title @>), [ maxLength 100 ]) TextProp ((fun f -> <@ f.ArtUrl @>), [ maxLength 100 ]) DecimalProp ((fun f -> <@ f.Price @>), [ min 0.01M; max 100.0M; step 0.01M ]) ], [])
Multiple items
val decimal : value:'T -> decimal (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.decimal

--------------------
type decimal = System.Decimal

Full name: Microsoft.FSharp.Core.decimal

--------------------
type decimal<'Measure> = decimal

Full name: Microsoft.FSharp.Core.decimal<_>
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = System.String

Full name: Microsoft.FSharp.Core.string
val min : e1:'T -> e2:'T -> 'T (requires comparison)

Full name: Microsoft.FSharp.Core.Operators.min
val max : e1:'T -> e2:'T -> 'T (requires comparison)

Full name: Microsoft.FSharp.Core.Operators.max

Album type contains all fields needed for the form. For the moment, Suave.Form supports following types of fields:

  • decimal
  • string
  • System.Net.Mail.MailAddress
  • Suave.Form.Password

Note: the int type is not supported yet, however we can easily convert from decimal to int and vice versa

Afterwards comes a declaration of the album form (let album : Form<Album> =). It consists of list of "Props" (Properties), of which we can think as of validations:

  • First, we declared that the Title must be of max length 100
  • Second, we declared the same for ArtUrl
  • Third, we declared that the Price must be between 0.01 and 100.0 with a step of 0.01 (this means that for example 1.001 is invalid)

Those properties can be now used as both client and server side. For client side we will the album declaration in our View module in order to output HTML5 input validation attributes. For server side we will use an utility WebPart that will parse the form field values from a request.

Note: the above snippet uses F# Quotations - a feature that you can read more about here. For the sake of tutorial, you only need to know that they allow Suave to lookup the name of a Field from a property getter.

To see how we can use the form in View module, add open Suave.Form to the beginning:

open System open Suave.Html open Suave.Form
namespace System

Next, add a couple of helper functions:

let divClass c = divAttr ["class", c]
val divClass : c:'a -> 'b

Full name: CDocument.divClass
val c : 'a

let fieldset x = tag "fieldset" [] (flatten x) let legend txt = tag "legend" [] (text txt)
val fieldset : x:'a -> 'b

Full name: CDocument.fieldset
val x : 'a
val legend : txt:'a -> 'b

Full name: CDocument.legend
val txt : 'a

And finally this block of code:

type Field<'a> = { Label : string Xml : Form<'a> -> Suave.Html.Xml } type Fieldset<'a> = { Legend : string Fields : Field<'a> list } type FormLayout<'a> = { Fieldsets : Fieldset<'a> list SubmitText : string Form : Form<'a> } let renderForm (layout : FormLayout<_>) = form [ for set in layout.Fieldsets -> fieldset [ yield legend set.Legend for field in set.Fields do yield divClass "editor-label" [ text field.Label ] yield divClass "editor-field" [ field.Xml layout.Form ] ] yield submitInput layout.SubmitText ]
type Field<'a> =
  {Label: string;
   Xml: obj -> obj;}

Full name: CDocument.Field<_>
Field.Label: string
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = System.String

Full name: Microsoft.FSharp.Core.string
Field.Xml: obj -> obj
type Fieldset<'a> =
  {Legend: string;
   Fields: Field<'a> list;}

Full name: CDocument.Fieldset<_>
Fieldset.Legend: string
Fieldset.Fields: Field<'a> list
type 'T list = List<'T>

Full name: Microsoft.FSharp.Collections.list<_>
type FormLayout<'a> =
  {Fieldsets: Fieldset<'a> list;
   SubmitText: string;
   Form: obj;}

Full name: CDocument.FormLayout<_>
FormLayout.Fieldsets: Fieldset<'a> list
FormLayout.SubmitText: string
FormLayout.Form: obj
val renderForm : layout:FormLayout<'a> -> 'b

Full name: CDocument.renderForm
val layout : FormLayout<'a>
val set : elements:seq<'T> -> Set<'T> (requires comparison)

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.set

Above snippet is quite long but, as we'll soon see, we'll be able to reuse it a few times. The FormLayout types defines a layout for a form and consists of:

  • SubmitText that will be used for the string value of submit button
  • Fieldsets - a list of Fieldset values
  • Form - instance of the form to render

The Fieldset type defines a layout for a fieldset:

  • Legend is a string value for a set of fields
  • Fields is a list of Field values

The Field type has:

  • a Label string
  • Xml - function which takes Form and returns Xml (object model for HTML markup). It might seem cumbersome, but the signature is deliberate in order to make use of partial application

Note: all of above types are generic, meaning they can accept any type of form, but the form's type must be consistent in the FormLayout hierarchy.

renderForm is a reusable function that takes an instance of FormLayout and returns HTML object model:

  • it creates a form element
  • the form contains a list of fieldsets, each of which:
    • outputs its legend first
    • iterates over its Fields and
      • outputs div element with label element for the field
      • outputs div element with target input element for the field
  • the form ends with a submit button

renderForm ca be invoked like this:

let createAlbum genres artists = [ h2 "Create" renderForm { Form = Form.album Fieldsets = [ { Legend = "Album" Fields = [ { Label = "Genre" Xml = selectInput (fun f -> <@ f.GenreId @>) genres None } { Label = "Artist" Xml = selectInput (fun f -> <@ f.ArtistId @>) artists None } { Label = "Title" Xml = input (fun f -> <@ f.Title @>) [] } { Label = "Price" Xml = input (fun f -> <@ f.Price @>) [] } { Label = "Album Art Url" Xml = input (fun f -> <@ f.ArtUrl @>) ["value", "/placeholder.gif"] } ] } ] SubmitText = "Create" } div [ aHref Path.Admin.manage (text "Back to list") ] ]
val createAlbum : genres:'a -> artists:'b -> 'c list

Full name: CDocument.createAlbum
val genres : 'a
val artists : 'b
union case Option.None: Option<'T>

We can see that for the Xml values we can invoke selectInput or input functions. Both of them take as first argument function which directs to field for which the input should be generated. input takes as second argument a list of optional attributes (of type string * string - key and value). selectInput takes as second argument list of options (of type decimal * string - value and display name). As third argument, selectInput takes an optional selected value - in case of None, the first one will be selected initially.

Note: We are hardcoding the album's ArtUrl property with "/placeholder.gif" - we won't implement uploading images, so we'll have to stick with a placeholder image.

Now that we have the createAlbum view, we can write the appropriate WebPart handler. Start by adding getArtists to Db:

type Artist = DbContext.``[dbo].[Artists]Entity``
type Artist = obj

Full name: CDocument.Artist

let getArtists (ctx : DbContext) : Artist list = ctx.``[dbo].[Artists]`` |> Seq.toList
val getArtists : ctx:'a -> 'b list

Full name: CDocument.getArtists
val ctx : 'a
type 'T list = List<'T>

Full name: Microsoft.FSharp.Collections.list<_>
module Seq

from Microsoft.FSharp.Collections
val toList : source:seq<'T> -> 'T list

Full name: Microsoft.FSharp.Collections.Seq.toList

Then proper entry in Path module:

let createAlbum = "/admin/create"
val createAlbum : string

Full name: CDocument.createAlbum

and WebPart in App module:

let createAlbum = let ctx = Db.getContext() choose [ GET >=> warbler (fun _ -> let genres = Db.getGenres ctx |> List.map (fun g -> decimal g.GenreId, g.Name) let artists = Db.getArtists ctx |> List.map (fun g -> decimal g.ArtistId, g.Name) html (View.createAlbum genres artists))
val createAlbum : obj

Full name: CDocument.createAlbum
val ctx : obj
Multiple items
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
  interface IEnumerable
  interface IEnumerable<'T>
  member GetSlice : startIndex:int option * endIndex:int option -> 'T list
  member Head : 'T
  member IsEmpty : bool
  member Item : index:int -> 'T with get
  member Length : int
  member Tail : 'T list
  static member Cons : head:'T * tail:'T list -> 'T list
  static member Empty : 'T list

Full name: Microsoft.FSharp.Collections.List<_>
val map : mapping:('T -> 'U) -> list:'T list -> 'U list

Full name: Microsoft.FSharp.Collections.List.map
Multiple items
val decimal : value:'T -> decimal (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.decimal

--------------------
type decimal = System.Decimal

Full name: Microsoft.FSharp.Core.decimal

--------------------
type decimal<'Measure> = decimal

Full name: Microsoft.FSharp.Core.decimal<_>

path Path.Admin.createAlbum >=> createAlbum

Once again, warbler will prevent from eager evaluation of the WebPart - it's vital here. To our View.manage we can add a link to createAlbum:

let manage (albums : Db.AlbumDetails list) = [ h2 "Index" p [ aHref Path.Admin.createAlbum (text "Create New") ]
val manage : albums:'a -> 'b list

Full name: CDocument.manage
val albums : 'a
type 'T list = List<'T>

Full name: Microsoft.FSharp.Collections.list<_>

This allows us to navigate to "/admin/create", however we're still lacking the actual POST handler.

Before we define the handler, let's add another helper function to App module:

let bindToForm form handler = bindReq (bindForm form) handler BAD_REQUEST
val bindToForm : form:'a -> handler:'b -> 'c

Full name: CDocument.bindToForm
val form : 'a
val handler : 'b

It requires a few modules to be open, namely:

  • Suave.Form
  • Suave.Http.RequestErrors
  • Suave.Model.Binding

What bindToForm does is:

  • it takes as first argument a form of type Form<'a>
  • it takes as second argument a handler of type 'a -> WebPart
  • if the incoming request contains form fields filled correctly, meaning they can be parsed to corresponding types, and hold all Props defined in Form module, then the handler argument is applied with the values of 'a filled in
  • otherwise the 400 HTTP Status Code "Bad Request" is returned with information about what was malformed.

There are just 2 more things before we're good to go with creating album functionality.

We need createAlbum for the Db module (the created album is piped to ignore function, because we don't need it afterwards):

let createAlbum (artistId, genreId, price, title) (ctx : DbContext) = ctx.``[dbo].[Albums]``.Create(artistId, genreId, price, title) |> ignore ctx.SubmitUpdates()
val createAlbum : artistId:'a * genreId:'b * price:'c * title:'d -> ctx:'e -> 'f

Full name: CDocument.createAlbum
val artistId : 'a
val genreId : 'b
val price : 'c
val title : 'd
val ctx : 'e
val ignore : value:'T -> unit

Full name: Microsoft.FSharp.Core.Operators.ignore

as well as POST handler inside the createAlbum WebPart:

let createAlbum = let ctx = Db.getContext() choose [ GET >=> warbler (fun _ -> let genres = Db.getGenres ctx |> List.map (fun g -> decimal g.GenreId, g.Name) let artists = Db.getArtists ctx |> List.map (fun g -> decimal g.ArtistId, g.Name) html (View.createAlbum genres artists)) POST >=> bindToForm Form.album (fun form -> Db.createAlbum (int form.ArtistId, int form.GenreId, form.Price, form.Title) ctx Redirection.FOUND Path.Admin.manage) ]
val createAlbum : obj

Full name: CDocument.createAlbum
val ctx : obj
Multiple items
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
  interface IEnumerable
  interface IEnumerable<'T>
  member GetSlice : startIndex:int option * endIndex:int option -> 'T list
  member Head : 'T
  member IsEmpty : bool
  member Item : index:int -> 'T with get
  member Length : int
  member Tail : 'T list
  static member Cons : head:'T * tail:'T list -> 'T list
  static member Empty : 'T list

Full name: Microsoft.FSharp.Collections.List<_>
val map : mapping:('T -> 'U) -> list:'T list -> 'U list

Full name: Microsoft.FSharp.Collections.List.map
Multiple items
val decimal : value:'T -> decimal (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.decimal

--------------------
type decimal = System.Decimal

Full name: Microsoft.FSharp.Core.decimal

--------------------
type decimal<'Measure> = decimal

Full name: Microsoft.FSharp.Core.decimal<_>
Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

--------------------
type int = int32

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>


GitHub commit: 79c56f6521df52b449bbd2ece994e13e342a9b0c

Files changed:

  • App.fs (modified)
  • Db.fs (modified)
  • Form.fs (added)
  • Path.fs (modified)
  • SuaveMusicStore.fsproj (modified)
  • View.fs (modified)

results matching ""

    No results matching ""