3
comments
on 7/22/2014 4:17 AM

Structuring Single-Page Applications with WebSharper.UI.Next

Since our last post, we have been hard at work extending the basic functionality of WebSharper.UI.Next to include some higher-level things, in particular support for animation, and some functional abstractions which make creating single-page applications (SPAs) safer, easier, and more intuitive. In this post, we'll talk a bit about some of the web abstractions we've added; Anton has written about the animation functionality here.

For the uninitiated, WebSharper.UI.Next is our upcoming framework for creating rich single-page applications. By creating reactive variables, and views which allow these variables to be observed as and when they change, it is possible to create highly-reactive applications backed by a purely-functional model. You can see some examples of this framework at work here, and the GitHub repository showing the source code for the website here.

A key advantage of using WebSharper to develop applications is the use of F#'s type system not only to provide better safety guarantees --- that is, fewer runtime errors and less JavaScript debugging --- but also to help better structure applications. To this end, IntelliFactory have made use of approaches such as Formlets to better structure handling user input, and developed our own such as Flowlets and Piglets. This past week, we've been working on seeing how some of these fit with SPA applications, and showing how they can be used within UI.Next. In particular, this past week we have looked at Flowlets, which help with structuring parts of an application which have a linear control flow, and how UI.Next can help to structure sites without such a control flow. Additionally, we've looked at how it's possible to synchronise the current position in a single-page application with the URL bar, to make sharing pages easier.

Flowlets

Flowlets are built to make it easier to structure applications (or indeed segments of an application: everything is composable) which have a linear control flow. By way of example, think for example of a registration form which has multiple pages, each getting different pieces of information, where each page may depend on information provided by a previous page.

In our small example, we first ask the user for some common information: in this case, a name and address. After this information is provided, we then ask the user whether they wish to specify a name or a phone number. Depending on which option is selected, we then display a different form to retreive the required information, before displaying the results on a final page. You can find this live here, and the source code here. The source code contains a bit of extra markup, but we'll concentrate on the main specifics here instead.

Firstly, we define our model, containing the data which we wish to retrieve. A key advantage of using F# for this kind of thing is that we can work directly with a model we have created in F#, without having to think about JavaScript objects and such.

1
2
3
4
5
6
7
8
9
10
11
type Person =
    {
        Name : string
        Address : string
    }

type ContactType = | EmailTy | PhoneTy

type ContactDetails =
    | Email of string
    | PhoneNumber of string

The Person type consists of a name and address. The ContactType type specifies whether the user has opted to specify an e-mail address or a phone number, and the ContactDetails discriminated union specifies the data which has been submitted.

Now, with the model created, we can look at how everything fits together.

To create a flowlet, we use the Define function.

1
val Define : (('A -> unit) -> Doc) -> Flow<'A> 

Here, (('A -> unit) -> Doc) is a function which takes a callback of type ('A -> unit), which is used to progress to the next stage of the flowlet, and produces a Doc. For readers unfamiliar with Doc, this is our monoidally-defined representation of a fragment of a reactive DOM tree, used in this case to render the flowlet. This in turn produces a value of type Flow<'A>, where 'A is the type of data returned by that flowlet page.

Now, we create some pages. As I say, I've stripped out the markup, concentrating on the bits of the code we care about.

Firstly, here's the first page which grabs the name and address from the user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let personFlowlet =
    Flow.Define (fun cont ->
        let rvName = Var.Create ""
        let rvAddress = Var.Create ""

        Doc.Input [] rvName 
        Doc.Input [] rvAddress 
        Doc.Button "Next" [cls "btn" ; cls "btn-default"] (fun () ->
            let name = Var.Get rvName
            let addr = Var.Get rvAddress
            // We use the continuation function to return the
            // data we retrieved from the form.
            cont ({Name = name ; Address = addr})
        )
    )

The personFlowlet function is of type Flow<Person>: a flowlet segment returning a value of type Person. We begin the function with Flow.Define, which takes a continuation function cont. The next thing to do is specify reactive variables for the name and address fields, initialised with the empty string.

Once we have the variables, we can use these with the Doc.Input function to create a form control which populates the variables. The submission button is then created using a function which gets the current calues of the name and address fields, and then passes a Person value to the continuation function.

Using the same technique, we can then make three further pages: contactTypeFlowlet, which returns a value of type ContactType; contactFlowlet, which takes a ContactType and returns a ContactDetails value, and finally a static page which displays the details.

After this is complete, all that is left is to tie everything together. Making use of the fact that flowlets have a monadic interface, we can construct flowlets using a computation expression. By doing this, we specify the order in which the pages appear, and how the data collected from one page may be used in another.

1
2
3
4
5
6
7
8
let PersonContactFlowlet =
    Flow.Do {
        let! person = personFlowlet
        let! ct = contactTypeFlowlet
        let! contactDetails = contactFlowlet ct
        return! Flow.Static (finalPage person contactDetails)
    }
    |> Flow.Embed

In this computation expression, preson is of type Person, ct is of type contactTypeFlowlet, and contactDetails is of type ContactDetails. It's also clear that the contactFlowlet flowlet depends on the value retrieved from the contactTypeFlowlet flowlet page. Finally, we return a static page displaying the details, and then use the Flow.Embed combinator to embed the flowlet into a reactive DOM fragment of type Doc, which can be composed as normal.

Structuring Non-Linear Sites

Flowlets are great for specifying parts of an application which have a linear control flow. They don't work so well when we want to jump around freely between different parts, however.

Using UI.Next, it's also easy to construct SPA applications consisting of multiple 'pages', which are loaded in a structured manner. In particular, by using UI.Next, everything can be specified without thinking about URLs at all, but purely passing the page we want to load as a continuation function. It's best to hop straight in with an example. Let's make a small, silly website.

To start with, we need to create a discriminated union representing each page in the site.

1
2
3
4
5
type Page =
    | BobsleighHome
    | BobsleighHistory
    | BobsleighGovernance
    | BobsleighTeam

By creating the Page type, we'll then be able to create a Var<Page> which specifies the current page we're on. We'll consider two different types of navigation: global navigation performed by a navigation bar, and local navigation, performed by links on individual pages.

Now, we'll make implementations of each page. Each page will take a record, Context containing a continuation function, which can be used to change the current page. For example, here is the source for the home page:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    type Context = {
        Go : Page -> unit
    }

    let HomePage ctx =
        Doc.Concat [
            el "div" [
                el "h1" [txt "Welcome!"]
                el "p" [
                    txt "Welcome to the IntelliFactory Bobsleigh MiniSite! Here you can find out about the "
                    link "history" [] (fun () -> ctx.Go BobsleighHistory)
                    txt " of bobsleighs, the "
                    link "International Bobsleigh and Skeleton Federation" [] (fun () -> ctx.Go BobsleighGovernance)
                    txt ", which serve as the governing body for the sport, and finally the world-famous "
                    link "IntelliFactory Bobsleigh Team." [] (fun () -> ctx.Go BobsleighTeam)
                ]
            ]
        ]

You'll notice here that each link is associated with a callback, which executes the ctx.Go function, providing the next page to show.

Tying this all together, we get a main function which looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 let Main () =
    let m = Var.Create BobsleighHome

    let withNavbar =
        Doc.Append (NavBar m)

    let ctx = {Go = Var.Set m}
    View.FromVar m
    |> View.Map (fun pg ->
        match pg with
        | BobsleighHome -> HomePage ctx |> withNavbar
        | BobsleighHistory -> History ctx |> withNavbar
        | BobsleighGovernance -> Governance ctx |> withNavbar
        | BobsleighTeam -> Team ctx |> withNavbar)
    |> Doc.EmbedView

Here, we create a context instance with Var.Set m as the continuation function. We then create a view of the page variable, which we can use to update the display of the page whenever it changes. In particular, through the use of View.Map, we change a View<Page> and create a View<Doc>. This can then be embedded into the reactive DOM using the Doc.EmbedView : View<Doc> -> Doc combinator.

Global actions, for example if we have a shared component such as a navigation bar, can be created by passing the page variable as a parameter. To change the current page, it's just necessary to set the variable.

URL Routing

Finally, SPA applications often lose the ability to do such things as using the 'back' button to go back in the application, or use the URL to share the current point in the application. We've worked on this to create a "Site" abstraction to allow different parts of the site to be responsible for different parts of the URL: the key here is that these sites are composable, meaning that it is possible to have different sub-sites.

The key idea behind this is the Router<'T> type, which essentially specifies a bijection between fragments of the URL, and page representations such as Page in the site above. If we were to want to extend the bobsleigh mini-site with some kind of URL routing, a router could be created as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    let TheRouter =
        Router.Create
            (function
                | BobsleighHome -> Route.Create []
                | BobsleighHistory -> Route.Create [RouteFrag.Create "history"]
                | BobsleighGovernance -> Route.Create [RouteFrag.Create "governance"]
                | BobsleighTeam -> Route.Create [RouteFrag.Create "team"])
            (fun route ->
                match Route.ToStringList route with
                | [] -> BobsleighHome
                | ["history"] -> BobsleighHistory
                | ["governance"] -> BobsleighGovernance
                | ["team"] -> BobsleighTeam
                | xs -> BobsleighHome)

We could then use the resulting router to create a Site, which is then merged with other Sites, to create a routing trie. If you want to see this in action, check out the live example, and you can also take a look at the source here. Indeed we use this technique on our main samples site itself!

The Future

Our goals now are to get some more functional abstractions added in, particularly those used for handling user input such as forms. In particular, it would be nice to see how we can integrate Loic Denuziere's work on Piglets to make use of the reactive backend, instead of using the stream abstraction. Additionally, we're adding features and examples all the time, so keep checking back!

.

This looks really nice. I've long appreciated how easy flowlets makes building wizard-style forms. A few points to note:

  1. The Router seems to require double effort to map back and forth. It would be nice to declare the the mapping between the Page and string fragments once.
  2. Why is a route a string list, or at least why is it converted as such?
  3. It seems as though the mapping is actually triplicate, since you also have to map to a Page instance. Would it be possible instead to use a (string * 'TPage * (Context -> Doc)) list to define the list of routes available to the Router with some way of designating the default route?

In addition, I recently found a post describing a similar solution with AngularJS and angular-ui-router. I would find it interesting to see how you would tie in custom animation to the flowlet defined above.

UI-Router is itself an interesting study as it allows navigation by any state, not just URI fragment/history API changes and nesting. I would assume UI.Next would support this sort of thing very well, but the router above seems tied to the URI. How might I define a route based on application state rather than the URI? Would it compose with the router defined above?

Thanks for your excellent work on this project!

By on 7/24/2014 9:53 PM ()

Hi Ryan, thanks for your interest and your comments.

We're still working on the routing API -- in fact there have been a few changes since writing this post. Let me address your points in turn:

1) Agreed here -- some kind of helper function to declare a mapping between the two (with a fallback to a default page of some kind) would be useful, and save some writing.

2) A route is a string list as we envisage that most routes will be a slash-separated hash-location in the URL. The routing itself is handled using a trie structure, where these fragments are used to perform intermediate lookups.

3) Quite possibly, but we're trying to keep the API as flexible as possible right now, even if it means that some code is a tad more verbose. If we were to add such functionality, it would likely be as a helper function in the library as opposed to a replacement.

It's interesting you should mention about the custom animation and flowlets, we have already briefly thought about this :) Watch this space.

With regard to your final point, you should be able to use the standard UI.Next tools (Vars, Views and so on) along with your F# model to route things according to state pretty easily -- this router mainly exists to aid people by providing the synchronisation between a URL route and some Var describing the current place within the application. You could quite easily change that Var within your application logic, and the router would update the URL on account of its bidirectionality, so composability shouldn't be an issue here.

Once again, thanks very much for the comment and I hope I've answered some of your questions (and if I haven't, feel free to reply and tell me that :))

We'll be shipping a preliminary release very soon indeed, so you should be able to have a play, and we'd very much appreciate any further comments!

By on 7/25/2014 1:24 AM ()

I have an existing websharper MVC site. Is there a way to use UI.Next side by side with my existing code, or would i need to start all over with UI.Next? For example, if I create a one page app in UI Next, How do I call it from my MVC site? I tried creating a Web.Control(), but the Doc type is not compatible with pagelets.

By on 10/20/2014 9:01 AM ()
IntelliFactory Offices Copyright (c) 2011-2012 IntelliFactory. All rights reserved.
Home | Products | Consulting | Trainings | Blogs | Jobs | Contact Us | Terms of Use | Privacy Policy | Cookie Policy
Built with WebSharper