Fabulous - Guide

Pages

ContentPage

A single page app typically returns a ContentPage. For example:

let view model dispatch =
    View.ContentPage(
        title = "Pocket Piggy Bank",
        content = View.Label(text = sprintf "Hello world!")
    )

For other kinds of pages, see Multi-page Applications and Navigation

See also:

Layouts

Xamarin.Forms has several layouts and features for organizing content on screen. For a comprehensive guide see the Xamarin Guide to Layouts

Xamarin.Forms Layouts

StackLayout

StackLayout organizes views in a one-dimensional line (“stack”), either horizontally or vertically. Views in a StackLayout can be sized based on the space in the layout using layout options. Positioning is determined by the order views were added to the layout and the layout options of the views.

View.StackLayout(
    padding=20.0,
    children = [
        View.Label(text = sprintf "Welcome to the bank!")
        View.Label(text = sprintf "Balance: %s%.2f" model.CurrencySymbol model.Balance)
    ]
)

See also:

AbsoluteLayout

AbsoluteLayout positions and sizes child elements proportional to its own size and position or by absolute values. Child views may be positioned and sized using proportional values or static values, and proportional and static values can be mixed.

View.AbsoluteLayout(
    backgroundColor = Color.Blue.WithLuminosity(0.9),
    children = [
       View.Label(text = "Top Left", textColor = Color.Black)
           .LayoutFlags(AbsoluteLayoutFlags.PositionProportional)
           .LayoutBounds(Rectangle(0.0, 0.0, AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize))
       View.Label(text = "Centered", textColor = Color.Black)
           .LayoutFlags(AbsoluteLayoutFlags.PositionProportional)
           .LayoutBounds(Rectangle(0.5, 0.5, AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize))
       View.Label(text = "Bottom Right", textColor = Color.Black)
           .LayoutFlags(AbsoluteLayoutFlags.PositionProportional)
           .LayoutBounds(Rectangle(1.0, 1.0, AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize))
    ]
)

See also:

RelativeLayout

RelativeLayout is used to position and size views relative to properties of the layout or sibling views. Unlike AbsoluteLayout, RelativeLayout does not have the concept of the moving anchor and does not have facilities for positioning elements relative to the bottom or right edges of the layout. RelativeLayout does support positioning elements outside of its own bounds.

An example RelativeLayout is as follows:

View.RelativeLayout(
    children =
        [ View.Label(text = "RelativeLayout Example", textColor = Color.Red)
              .XConstraint(Constraint.RelativeToParent(fun parent -> 0.0))
          View.Label(text = "Positioned relative to my parent", textColor = Color.Red)
              .XConstraint(Constraint.RelativeToParent(fun parent -> parent.Width / 3.0))
              .YConstraint(Constraint.RelativeToParent(fun parent -> parent.Height / 2.0))
        ]
)

See also:

FlexLayout

FlexLayout is similar to the Xamarin.Forms StackLayout in that it can arrange its children horizontally and vertically in a stack. However, the FlexLayout is also capable of wrapping its children if there are too many to fit in a single row or column, and also has many options for orientation, alignment, and adapting to various screen sizes.

View.FlexLayout(
    direction=FlexDirection.Column,
    children = [
        View.Label(text = "Seated Monkey", fontSize="Large", textColor=Color.Blue)
        View.Label(text = "This monkey is laid back and relaxed.")
        View.Label(text = "  - Often smiles mysteriously")
        View.Label(text = "  - Sleeps sitting up")

        View.Image(widthRequest = 160.0, heightRequest = 240.0,
            source = "images/160px-Vervet_monkey_Krugersdorp_game_reserve_%285657678441%29.jpg"
        ).FlexOrder(-1).FlexAlignSelf(FlexAlignSelf.Center)

        View.Label(margin = Thickness(0.0, 4.0)).FlexGrow(1.0)
        View.Button(text = "Learn More", fontSize = "Large", cornerRadius = 20)
    ]
)

See also:

Grid

Grid supports arranging views into rows and columns. Rows and columns can be set to have proportional sizes or absolute sizes. The Grid layout should not be confused with traditional tables and is not intended to present tabular data. Grid does not have the concept of row, column or cell formatting. Unlike HTML tables, Grid is purely intended for laying out content.

An example Grid is as follows:

View.Grid(
    rowdefs = [for i in 1 .. 6 -> box "auto"],
    coldefs = [for i in 1 .. 6 -> box "auto"],
    children = [
        for i in 1 .. 6 do
            for j in 1 .. 6 ->
                let color = Color((1.0/float i), (1.0/float j), (1.0/float (i+j)), 1.0)
                View.BoxView(color).GridRow(i-1).GridColumn(j-1)
    ]
)

Notes:

See also:

ScrollView

ScrollView contains layouts and enables them to scroll offscreen. ScrollView is also used to allow views to automatically move to the visible portion of the screen when the keyboard is showing.

View.ScrollView(View.StackLayout(padding=20.0, children= ...) )

The scroll position can be setted programmatically through the attribute scrollTo. This attribute needs the X and Y coordinates to scroll to and an indication whether it should be animated or not. (Animated/NotAnimated)

Note: Fabulous will try to scroll to these coordinates every time it needs to refresh the UI. Making use of the optional argument is recommended.

You can also subscribe to the event Scrolled to be notified when the scrolling is over.

View.ScrollView(content=(...),
    ?scrollTo=(if model.ShouldScroll then Some (500.0, 0.0, Animated) else None),
    scrolled=(fun args -> dispatch Scrolled))

For more complex scenarios, you can directly use the method from Xamarin.Forms ScrollView.ScrollToAsync(x, y, animated) This method offers the advantage of being awaitable until the end of the scrolling. To do this, a reference to the underlying ScrollView is needed.

let scrollViewRef = ViewRef<ScrollView>()

View.ScrollView(ref=scrollViewRef, content=(...))

// Some time later (usually in a Cmd)
let scrollToCoordinates x y animated =
    async {
        match scrollViewRef.TryValue with
        | None ->
            return None
        | Some scrollView ->
            do! scrollView.ScrollToAsync(x, y, animated) |> Async.AwaitTask
            return (Some Scrolled)
    } |> Cmd.ofAsyncMsgOption

See also:

Lists and Tables

ListView, ListGroupedView

A simple ListView is as follows:

View.ListView(
    items = [ View.Label "Ionide"
              View.Label "Visual Studio"
              View.Label "Emacs"
              View.Label "Visual Studio Code"
              View.Label "JetBrains Rider"],
    itemSelected=(fun idx -> dispatch (ListViewSelectedItemChanged idx)))

In the underlying implementation, each visual item is placed in a ContentCell. Currently the itemSelected callback uses integers indexes for keys to identify the elements (NOTE: this may change in future updates).

There is also a ListViewGrouped for grouped items of data. This uses the same Xamarin control under the hood but in a different mode of use.

See also:

TableView

An example TableView is as follows:

View.TableView(
    items = [
        ("Videos", [ View.SwitchCell(on=true, text="Luca 2008", onChanged=(fun args -> ()) )
                     View.SwitchCell(on=true, text="Don 2010", onChanged=(fun args -> ()) ) ] )
        ("Books", [ View.SwitchCell(on=true, text="Expert F#", onChanged=(fun args -> ()) )
                    View.SwitchCell(on=false, text="Programming F#", onChanged=(fun args -> ()) ) ])
        ("Contact", [ View.EntryCell(label="Email", placeholder="foo@bar.com", completed=(fun args -> ()) )
                      View.EntryCell(label="Phone", placeholder="+44 87654321", completed=(fun args -> ()) )] )])

See also:

“Infinite” or “unbounded” ListViews

“Infinite” (really “unbounded”) lists are created by using the itemAppearing event to prompt a message which nudges the underlying model in a direction that will then supply new items to the view.

For example, consider this pattern:

type Model =
    { ...
      LatestItemAvailable: int
    }

type Message =
    ...
    | GetMoreItems of int

let update msg model =
    match msg with
    | ...
    | GetMoreItems n -> { model with LatestItemAvailable = n }

let view model dispatch =
    ...
    View.ListView(
        items = [ for i in 1 .. model.LatestItemAvailable do
                     yield View.Label("Item " + string i) ],
        itemAppearing = (fun idx -> if idx >= max - 2 then dispatch (GetMoreItems (idx + 10) ) )  )
...

Note:

Surprisingly even this naive technique is fairly efficient. There are numerous ways to make this more efficient (we aim to document more of these over time too). One simple one is to memoize each individual visual item using dependsOn:

        items = [ for i in 1 .. model.LatestItemAvailable do
                    yield dependsOn i (fun model i -> View.Label("Item " + string i)) ]

With that, this simple list views scale to > 10,000 items on a modern phone, though your mileage may vary. There are many other techniques (e.g. save the latest collection of visual element descriptions in the model, or to use a ConditionalWeakTable to associate it with the latest model). We will document further techniques in due course.

Thre is also an itemDisappearing event for ListView that can be used to discard data from the underlying model and restrict the range of visual items that need to be generated.

Other Core Elements

All other controls from Xamarin.Forms.Core are available in the programming model. See the AllControls sample if the control is not documented here.

Button

Buttons are created using View.Button. The command of a button will normally dispatch a messsage. For example:

View.Button(text = "Deposit", command = (fun () -> dispatch (Add 10.0)))

See also:

Slider

A simple Slider is as follows:

View.Slider(
    minimum = 0.0,
    maximum = 10.0,
    value= double step,
    valueChanged=(fun args -> dispatch (SliderValueChanged (int (args.NewValue + 0.5))))
)

See also:

ActivityIndicator

A simple ActivityIndicator is as follows:

View.ActivityIndicator(isRunning = (count > 0))

See also:

DatePicker

A simple DatePicker is as follows:

View.DatePicker(minimumDate = DateTime.Today,
    maximumDate = DateTime.Today + TimeSpan.FromDays(365.0),
    date = startDate,
    dateSelected=(fun args -> dispatch (StartDateSelected args.NewDate)))

See also:

Editor

An example Editor is as follows:

View.Editor(text = editorText,
    textChanged = (fun args -> dispatch (TextChanged(args.OldTextValue, args.NewTextValue))),
    completed = (fun text -> dispatch (EditorEditCompleted text)))

See also:

BoxView

An example BoxView is as follows:

View.BoxView(Colors.Fuchsia)

See also:

Entry

An example Entry is as follows:

View.Entry(
    text = entryText,
    textChanged = (fun args -> dispatch (TextChanged(args.OldTextValue, args.NewTextValue))),
    completed = (fun text -> dispatch (EntryEditCompleted text))
)

An example Entry with password is as follows:

View.Entry(
    text = password,
    isPassword = true,
    textChanged = (fun args -> dispatch (TextChanged(args.OldTextValue, args.NewTextValue))),
    completed = (fun text -> dispatch (EntryEditCompleted text))
)

An example Entry with a placeholder is as follows:

View.Entry(
    placeholder = "Enter text",
    textChanged = (fun args -> dispatch (TextChanged(args.OldTextValue, args.NewTextValue))),
    completed = (fun text -> dispatch (EntryEditCompleted text))
)

See also:

Frame

A frame contains other content. A simple Frame is as follows:

View.Frame(hasShadow = true, backgroundColor = Colors.Fuchsia)

See also:

Image

A simple image drawn from a resource or URL is as follows:

let monkey = "http://upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Papio_anubis_%28Serengeti%2C_2009%29.jpg/200px-Papio_anubis_%28Serengeti%2C_2009%29.jpg"

View.Image(source = monkey)

See also:

Picker

A simple Picker is as follows:

let pickerItems =
    [| ("Aqua", Color.Aqua); ("Black", Color.Black);
       ("Blue", Color.Blue); ("Fucshia", Color.Fuchsia);
       ("Gray", Color.Gray); ("Green", Color.Green);
       ("Lime", Color.Lime); ("Maroon", Color.Maroon);
       ("Navy", Color.Navy); ("Olive", Color.Olive);
       ("Purple", Color.Purple); ("Red", Color.Red);
       ("Silver", Color.Silver); ("Teal", Color.Teal);
       ("White", Color.White); ("Yellow", Color.Yellow ) |]

View.Picker(
    title = "Choose Color:",
    textColor = snd pickerItems.[pickedColorIndex],
    selectedIndex = pickedColorIndex,
    itemsSource = Array.map fst pickerItems,
    selectedIndexChanged = (fun (i, item) -> dispatch (PickerItemChanged i))
)

See also:

A simple SearchBar is as follows:

View.SearchBar(
    placeholder = "Enter search term",
    searchCommand = (fun searchBarText -> dispatch  (ExecuteSearch searchBarText)),
    canExecute=true)

See also:

CollectionView, CarouselView, Shell

CollectionView, CarouselView and Shell are available in preview in Xamarin.Forms 3.5.
Please read the Xamarin.Forms documentation to check whether those controls are available for the platforms you target.

Fabulous provides an initial but partial support for them.
We will fully support them once officially released.

As they are experimental, each one of these controls requires a flag before they can be used.


// iOS
[<Register ("AppDelegate")>]
type AppDelegate () =
    inherit FormsApplicationDelegate ()

    override this.FinishedLaunching (uiApp, options) =
        Xamarin.Forms.Forms.SetFlags([|"Shell_Experimental"; "CollectionView_Experimental"|]);
        (...)

// Android
[<Activity>]
type MainActivity() =
    inherit FormsApplicationActivity()

    override this.OnCreate (bundle: Bundle) =
        base.OnCreate (bundle)
        global.Xamarin.Forms.Forms.SetFlags([|"Shell_Experimental"; "CollectionView_Experimental"|])
        (...)

Usage:

View.Shell(title = "TitleShell",
           items = [
               View.ShellItem(
                   items = [
                       View.ShellSection(items = [
                           View.ShellContent(title = "Section 1", content = View.ContentPage(content = View.Button(text = "Button")))         
                       ])
                   ])
           ])

View.CarouselView(itemsSource = [
            View.Label(text="Person1") 
            View.Label(text="Person2")
            View.Label(text="Person3")
            View.Label(text="Person4")
            View.Label(text="Person5")
        ])

View.CollectionView(items=[
            View.Label(text="Person1") 
            View.Label(text="Person2")
            View.Label(text="Person3")
            View.Label(text="Person4")
            View.Label(text="Person5")
        ])

See also:

Gestures

Gesture recognizers can be added to any visual element.

Tap Gestures

The tap gesture is used for tap detection. For example, here is a TapGestureRecognizer:

View.Frame(
    hasShadow = true,
    gestureRecognizers = [ View.TapGestureRecognizer(command=(fun () -> dispatch FrameTapped)) ]
)

See also:

Pan Gestures

The pan gesture is used for detecting dragging. A common scenario for the pan gesture is to horizontally and vertically drag an image, so that all of the image content can be viewed when it’s being displayed in a viewport smaller than the image dimensions. This is accomplished by moving the image within the viewport, and is demonstrated in this article.

Here is an example of a PanGestureRecognizer used to recognize panning touch movements:

View.Frame(
    hasShadow = true,
    gestureRecognizers = [
        View.PanGestureRecognizer(touchPoints=1, panUpdated=(fun panArgs ->
                if panArgs.StatusType = GestureStatus.Running then
                    dispatch (PanGesture panArgs)))
    ]
)

See also:

Pinch Gestures

The pinch gesture is used for performing interactive zoom. A common scenario for the pinch gesture is to perform interactive zoom of an image at the pinch location. This is accomplished by scaling the content of the viewport, and is demonstrated in this article.

Here is an example of a PinchGestureRecognizer used to recognize pinch-or-expand touch movements:

View.Frame(
    hasShadow=true,
    gestureRecognizers= [
        View.PinchGestureRecognizer(pinchUpdated=(fun pinchArgs ->
            dispatch (UpdateSize (pinchArgs.Scale, pinchArgs.Status))))
    ]
)

See also:

Pop-ups

Pop-ups are a special case in Fabulous: they are part of the view, but don’t follow the same lifecycle as the rest of the UI. In Xamarin.Forms pop-ups are exposed through 2 methods of the current page: DisplayAlert and DisplayActionSheet.

In Fabulous we only describe what a page should look like and have no access to UI elements. As such, there is no direct implementation of those 2 methods in Fabulous but instead we can use the static property Application.Current.MainPage exposed by Xamarin.Forms.

Here is an example of the use of a confirmation pop-up - note the requirement of Cmd.AsyncMsg so as not to block on the UI thread:

type Msg =
    | DisplayAlert
    | AlertResult of bool

let update (msg : Msg) (model : Model) =
    match msg with
    | DisplayAlert ->
        let alertResult = async {
            let! alert =
                Application.Current.MainPage.DisplayAlert("Display Alert", "Confirm", "Ok", "Cancel")
                |> Async.AwaitTask
            return AlertResult alert }

        model, Cmd.ofAsyncMsg alertResult

    | AlertResult alertResult -> ... // Do something with the result

Why don’t we add a Fabulous wrapper for those? Doing so would only end up duplicating the existing methods and compel us to maintain these in sync with Xamarin.Forms. See Pull Request #147 for more information

See also: