A sitelet consists of two parts; a router and a handler.
The job of the router is to map endpoints to URLs and to map HTTP requests to endpoints.
The handler is responsible for handling endpoints, by returning content (a synchronous or asynchronous HTTP response).
The router component of a sitelet can be constructed in multiple ways. The main options are:
Declaratively, using Router.Infer which is also used internally by Sitelets.Infer. The main advantage of creating a router value separately is that you can add a [<JavaScript>] attribute on it, so that the client can generate links from endpoint values too. WebSharper.UI contains functionality for client-side routing too, making it possible to handle all, or a subset of internal links without browser navigation. Sharing the router abstraction between client and server means that server can generate links that the client will handle and vice versa.
Manually, by using combinators to build up larger routers from elementary Router values or inferred ones. You can use this to further customize routing logic if you want a URL schema that is not fitting default inferred URL shapes, or add additional URLs to handle (e. g. for keeping compatibility with old links).
Implementing the IRouter interface directly or using the Router.New helper. This is the most universal way, but has less options for composition.
The following example shows how you can create a router of type WebSharper.Sitelets.IRouter<EndPoint> by writing the two mappings manually:
open WebSharper.Siteletsmodule WebSite = type EndPoint = | Page1 | Page2 let MyRouter : Router<EndPoint> = let route (req: Http.Request) = if req.Uri.LocalPath = "/page1" then Some Page1 elif req.Uri.LocalPath = "/page2" then Some Page2 else None let link endPoint = match endPoint with | EndPoint.Page1 -> Some <| System.Uri("/page1", System.UriKind.Relative) | EndPoint.Page2 -> Some <| System.Uri("/page2", System.UriKind.Relative) Router.New route link
A simplified version, Router.Create exists to create routers, using only already broken up URL segments:
open WebSharper.Siteletsmodule WebSite = type EndPoint = | Page1 | Page2 let MyRouter : Router<EndPoint> = let link endPoint = match endPoint with | Page1 -> [ "page1" ] | Page2 -> [ "page2" ] let route path = match path with | [ "page1" ] -> Some Page1 | [ "page2" ] -> Some Page2 | _ -> None Router.Create link route
Specifying routers manually gives you full control of how to parse incoming requests and to map endpoints to corresponding URLs. It is your responsibility to make sure that the router forms a bijection of URLs and endpoints, so that linking to an endpoint produces a URL that is in turn routed back to the same endpoint.
Constructing routers manually is only required for very special cases. The above router can for example be generated using Router.Table:
/ (alias Router.Combine): Parses or writes using two routers one after the other. For example rString / rInt will have type Router<string * int>. This operator has overloads for any combination of generic and non-generic routers, as well as a string on either side to add a constant URL fragment. For example r "article" / r "id" / rInt can be shortened to "article/id" / rInt.
+ (alias Router.Add): Parses or writes using the first router if successful, otherwise the second.
Router.Sum: Optimized version of combining a sequence of routers with +. Parses or writes with the first router in the sequence that can handle the path or value.
Router.Map: A bijection (or just surjection) between representations handled by routers. For example if you have a type Person = { Name: string; Age: int }, then you can define a router for it by mapping from a Router<string * int> like so
let rPerson : Router<Person> = rString / rInt |> Router.Map (fun (n, a) -> { Name = n; Age = a }) (fun p -> p.Name, p.Age)
See that Map needs two function arguments, to convert data back and forth between representations. All values of the resulting type must be mapped back to underlying type by the second function in a way compatible with the first function to work correctly.
Router.MapTo: Maps a non-generic Router to a single valued Router<'T>. For example if Home is a union case in your Pages union type describing pages on your site, you can create a router for it by:
let rHome : Router<Pages> = rRoot |> Router.MapTo Home
This only needs a single value as argument, but the type used must be comparable, so the writer part of the newly created Router<'T> can decide if it is indeed a Home value that it needs to write by the underlying router (in our case producing a root URL).
Router.Embed: An injection between representations handled by routers. For example if you have a Router<Person> parsing a person's details, and a Contact of Person union case in your Pages union, you can do:
let rContact : Router<Pages> = "contact" / rPerson |> Router.Embed Contact (function Contact p -> Some p | _ -> None)
See that now we have two functions again, but the second is returning an option. The first tells us that once a path is parsed (for example we are recognizing contact/Bob/32 here), it can wrap it in a Contact case (Contact here is used as a short version of a union case constructor, a function with signature Person -> Pages). And if the newly created router gets a value to write, it can use the second function to map it back optionally to an underlying value.
Router.Filter: restricts a router to parse/write values only that are passing a check. Usage: rInt |> Router.Filter (fun x -> x >= 0), which won't parse and write negative values.
Router.Slice: restricts a router to parse/write values only that can be mapped to a new value. Equivalent to using Filter first to restrict the set of values and then Map to convert to a type that is a better representation of the restricted values.
Router.TryMap: a combination of Slice and Embed, a mapping from a subset of source values to a subset of target values. Both the encode and decode functions must return None if there is no mapping to a value of the other type.
Router.Query: Modifies a router to parse from and write to a specific query argument instead of main URL segments. Usage: rInt |> Router.Query "x", which will read/write query segments like ?x=42. You should pass only a router that is always reading/writing a single segment, which inclide primitive routers, Router.Nullable, and Sums and Maps of these.
Router.QueryOption: Modifies a router to read an optional query value as an F# option. Creates a Router<option<'T>>, same restrictions apply as to Query.
Router.QueryNullable: Modifies a router to read an optional query value as a System.Nullable. Creates a Router<Nullable<'T>>, same restrictions apply as to Query.
Router.Box: Converts a Router<'T> to a Router<obj>. When writing, it uses a type check to see if the object is of type 'T so it can be passed to underlying router.
Router.Unbox: Converts a Router<obj> to a Router<'T>. When parsing, it uses a type check to see if the object is of type 'T so that the parsed value can be represented in 'T.
Router.Array: Creates an array parser/writer. The URL will contain the length and then the items, so for example Router.Array rString can handle 2/x/y.
Router.List: Creates a list parser/writer. Similar to Router.Array, just uses F# lists as data type.
Router.Option: Creates an F# option parser/writer. Writes or reads None and Some/x segments.
Router.Nullable: Creates a Nullable value parser/writer. Writes or reads null for null or a value that is handled by the input router. For
Router.Infer: Creates a router based on type shape. The attributes recognized are the same as Sitelet.Infer described in the sitelets documentation.
Router.Table: Creates a router mapping between a list of static endpoint values and paths.
Router.Method: Creates a router that only parses request with the inner router, it the HTTP method methes the given method argument. By default, routers ignore the method.
Router.Body : Creates a router that parses and serializes any value to and from the request body with custom functions. If the will be used on the server side only to parse requests and generate links, the serialize function can return just a null or empty string. For example Router.Body id id just gets the request body as a string.
Router.Json creates a router that parses the request body by the JSON format derived from the type argument.
Router.FormData creates a router from an underlying router handling query arguments that parses query arguments from the request body of a form post instead of the URL.
Router.Delay can be used to construct routers for recursive data types. Takes a unit -> Router<'T> function, and evaluates it firsthe t time the router is used for parsing and writing (never just when combining them).
Router.Link creates a (relative) link using a router.
A useful helper to have in the file defining your router is:
let Link page content = a [ attr.href (Router.Link router page) ] [ text content ]
This works the same on both server and client side to create basic <a> links to pages of your web application.
Sitelet.New creates a sitelet from a router and handler. Example:
let Main = Sitelet.New rPages (fun ctx ep -> match ep with | Home -> div [] [ text "This is the home page" ] | Contact _ -> client <@ ContactMain() @> )
Here we return a static page for the root, but call into a client-side generated content in the Contact pages, which is parsing the URL again to show the contact details from the URL.
Sitelets are only a server-side type.
Router.Ajax makes a request from an endpoint value on the client and executes it using jQuery.ajax. Returns an async<string>, which raises an exception internally if the request fails. Example:
// [<EndPoint "/get-data">] GetData of int let GetDataAsyncSafe i = async { try return! Some (Router.Ajax router (GetData i)) with _ -> None }