comment
I have been reworking an older WebSharper jQuery Mobile app which used one sitelet (compiled to HTML file) for each JQM page. I decided to use another approach: a single-page application with JQM pages generated lazily and reused if needed. JQM has a sample which that gives an example of page reuse, so I implemented it with WebSharper. The resulting framework is easy to expand and manage.
First some helper functions for building page content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
namespace Sample open IntelliFactory.WebSharper open IntelliFactory.WebSharper.JQuery open IntelliFactory.WebSharper.JQuery.Mobile open IntelliFactory.WebSharper.Html [<JavaScript>] module App = let mobile = Mobile.Instance let HeaderDiv cont = Div [ HTML5.Attr.Data "role" "header" ] -< cont let ContentDiv cont = Div [ HTML5.Attr.Data "role" "content" ] -< cont let PageDiv id' cont = Div [ HTML5.Attr.Data "role" "page" Id id' ] -< cont |>! OnAfterRender (fun el -> JQuery.Of el.Body |> Mobile.Page.Init ) let ListViewUL cont = UL [ HTML5.Attr.Data "role" "listview" HTML5.Attr.Data "inset" "true" ] -< cont
Then the sample data, now stored locally, but it could be modified easily to make a web request for example using WebSharper's automatic RPC implementation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
//module App continued type CategoryData = { Name : string Description : string Items : string list } let getCategoryData category = match category with | "animals" -> Some { Name = "Animals" Description = "All your favorites from aardvarks to zebras." Items = [ "Pets"; "Farm Animals"; "Wild Animals" ] } | "colors" -> Some { Name = "Colors" Description = "Fresh colors from the magic rainbow." Items = [ "Blue"; "Green"; "Orange"; "Purple"; "Red"; "Yellow"; "Violet" ] } | "vehicles" -> Some { Name = "Vehicles" Description = "Everything from cars to planes." Items = [ "Cars"; "Planes"; "Construction" ] } | _ -> None
Now, lets define the pages. A simple record can contain the DOM element of the page, and a function that should be run on page load (first use and reuse too). It returns a bool
deciding if we should proceed with the page change or cancel it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
//module App continued type JQMPage = { Html: Element Load: unit -> bool } // Page Ids module Ids = let [<Literal>] HomePage = "home" let [<Literal>] ItemsPage = "items" module Refs = let [<Literal>] HomePage = "#home" let [<Literal>] ItemsPage = "#items" // State let mutable selectedCategory = None let HomePage = let createListItem category text = LI [ A [ HRef ""; Text text ] |>! OnClick (fun _ _ -> selectedCategory <- Some category Refs.ItemsPage |> mobile.ChangePage ) ] { Html = PageDiv Ids.HomePage [ HeaderDiv [ H1 [ Text "Categories" ] ] ContentDiv [ H2 [ Text "Select a Category Below:" ] ListViewUL [ createListItem "animals" "Animals" createListItem "colors" "Colors" createListItem "vehicles" "Vehicles" ] ] ] Load = fun() -> true } let ItemsPage = lazy let title = H1 [] let description = P [] let itemsList = ListViewUL [] { Html = PageDiv Ids.ItemsPage [ HTML5.Attr.Data "add-back-btn" "true" ] -< [ HeaderDiv [ title ] ContentDiv [ description ListViewUL [ itemsList ] ] ] Load = fun() -> match getCategoryData selectedCategory.Value with | Some categoryData -> title.Text <- categoryData.Name description.Text <- categoryData.Description itemsList.Clear() categoryData.Items |> List.iter (fun i -> (LI [Text i]) |> itemsList.Append) JQuery.Of itemsList.Body |> Mobile.ListView.Refresh true | None -> false } let getJQMPage pageRef = match pageRef with | Refs.HomePage -> Some HomePage | Refs.ItemsPage -> Some ItemsPage.Value | _ -> None
Any pages can be initialized lazily, so the app loads as fast as possible. We will want to load the #home
page when the application starts, so there it is not needed. The helper function getJQMPage
matches the inner page hash URLs to the JQMPage
records we defined.
Finally we have to define a Web.Control
which handles the creating of pages on page change requests and loads the #home
page initially. The event handler for PageBeforeChange
checks if the DOM node for the required page has been inserted already and initializes it if it has'nt been. If Load()
would return false
we cancel the page change.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
type AppControl() = inherit Web.Control() [<JavaScript>] override this.Body = Mobile.Events.PageBeforeChange.On(JQuery.Of Dom.Document.Current, fun (e, data) -> match data.ToPage with | :? string as pageUrl -> match App.getJQMPage pageUrl with | Some pageObj -> let body = JQuery.Of "body" let toPage = match body.Children pageUrl with | p when p.Length = 0 -> let page = pageObj.Html body.Append page.Body |> ignore (page :> IPagelet).Render() JQuery.Of page.Body | p -> p if not (pageObj.Load()) then e.PreventDefault() | None _ -> () | _ -> () ) upcast Div [] |>! OnAfterRender (fun _ -> App.Refs.HomePage |> App.mobile.ChangePage)
WebSharper allows for an easy composition of dynamically created pages which can make creating mobile apps a quick and fun process.
Good job!