Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: adding child views at runtime to FlexLayout or StackLayout? #18

Open
gaelian opened this issue Jul 1, 2021 · 4 comments
Open

Comments

@gaelian
Copy link

gaelian commented Jul 1, 2021

Hello! I'm back with another possibly silly question.

I have a situation where I need to add child views to - preferably - a FlexLayout or StackLayout, at runtime, after the first init call, based on user input. The child views can be of different types, depending on the user input and values that are periodically retrieved/updated from a remote source and saved into an SQLite database in the app.

I had hoped that I could just wrap a FlexLayout in a ScrollView and when I insert the new child views, things would expand to accomodate, even if the overall height ends up taller than the device screen. It seems that FlexLayout does not do this. What I'm seeing is that the ScrollView does seem to account for the height of the newly inserted elements but neither a FlexLayout or StackLayout do. So the result is that after the new child views are inserted, any child views that end up below the overall height of the screen are not shown, but the ScrollView none the less acts like they are there and one can scroll "below the fold" to see only empty space.

Before I start tearing out code wholesale to try and find a completely different way to do this, can anyone point me in the direction of a good way to dynamically insert heterogeneous child views like this, with Fabulous?

A simplified and contrived example of what I'm trying to do (based off the Fabulous example app):

// Copyright Fabulous contributors. See LICENSE.md for license.
namespace FabXamApp

open System
open Fabulous
open Fabulous.XamarinForms
open Xamarin.Forms

module App = 
    type Model = 
      { Values: Tuple<string, string> list }

    type Msg = 
        | Insert
        | Reset

    let initModel = { Values = [] }

    let init () = initModel, Cmd.none

    let update msg model =
        match msg with
        | Insert ->
            // In reality these values come from a database and can change from time to time.
            // Sometimes there could be quite a lot of values, other times not so much.
            let values = [ ("Label 1", "Placeholder 1"); ("Label 2", "Placeholder 2"); ("Label 3", "Placeholder 3"); ("Label 4", "Placeholder 4") ]

            { model with Values = values }, Cmd.none
        | Reset -> init ()

    let insertView value = 
        match value with
        | v when v = ("Label 2", "Placeholder 2") ->
            View.Grid(
                width = 250.,
                height = 200.,
                rowdefs = [ Absolute 200. ],
                coldefs = [ Star; Star ],
                children = [
                    View.Label(
                        verticalOptions = LayoutOptions.FillAndExpand,
                        verticalTextAlignment = TextAlignment.Center,
                        text = (v |> fst)
                    ).Row(0).Column(0)
                    View.Switch(
                        verticalOptions = LayoutOptions.FillAndExpand,
                        horizontalOptions = LayoutOptions.End,
                        isToggled = true
                    ).Row(0).Column(1)
                ]
            )
        | _ ->
            View.StackLayout(
                height = 200.,
                horizontalOptions = LayoutOptions.Center,
                padding = Thickness (0., 10., 0., 10.),
                children = [
                    View.Label(
                        height = 42.,
                        width = 200.,
                        text = (value |> fst)
                    )
                    View.Editor(
                        height = 42.,
                        width = 200.,
                        placeholder = (value |> snd)
                    )
                ]
            )

    let view (model: Model) dispatch =
        View.ContentPage(
          content = 
            View.ScrollView(
                View.FlexLayout(
                    direction = FlexDirection.Column,
                    alignItems = FlexAlignItems.Center,
                    justifyContent = FlexJustify.SpaceEvenly,
                    children = [ 
                        // In reality, user input is more variable than just a button and this input will ultimately 
                        // decide whether labels, switches, entries, etc. are inserted.
                        View.Button(text = "Insert", horizontalOptions = LayoutOptions.Center, command = (fun () -> dispatch Insert))

                        View.Button(text = "Reset", horizontalOptions = LayoutOptions.Center, command = (fun () -> dispatch Reset))

                        match model.Values.Length with
                        | l when l > 0 ->
                            for value in model.Values do
                                insertView value
                        | _ -> ()
                        View.Label(text = "I'm near the bottom and may not be visible after you tap 'Insert'", horizontalOptions = LayoutOptions.Center, width=200.0, horizontalTextAlignment=TextAlignment.Center)
                    ]
                )
            )
        )

    // Note, this declaration is needed if you enable LiveUpdate
    let program =
        XamarinFormsProgram.mkProgram init update view
#if DEBUG
        |> Program.withConsoleTrace
#endif

type App () as app = 
    inherit Application ()

    let runner = 
        App.program
        |> XamarinFormsProgram.run app
@reigam
Copy link

reigam commented Jul 1, 2021

For what reason do you prefer Stack- or FlexLayout?

Alternatively consider:
Option A: use TableView and add Cells as needed (scrolling is baked in)
Option B: use Grid and add rows as needed (scrolling is baked in)
Option C: use CollectionView and add Layouts containing ChildViews as needed (scrolling is baked in)

@gaelian
Copy link
Author

gaelian commented Jul 1, 2021

For what reason do you prefer Stack- or FlexLayout?

@reigam it's mostly just where I ended up before this new "dynamically adding views" requirement came to light and I was hoping there was a way to make things work without what would more or less be a full overhaul of the page in question (which is a lot more complex than the toy example I provided).

Other than this issue, the behaviour of FlexLayout would have been perfect and easy for what I was trying to do. After now spending several evenings unsuccessfully trying to make this work, I already suspected I would need to cut my losses and go with a different approach. But before spending significant time on re-work, I wanted to get some idea of other's experience to make sure that this re-work would be on the right track. I was thinking about a Grid, but maybe CollectionView would be easier for this. I'll need to do some experimentation. Thanks.

@reigam
Copy link

reigam commented Jul 2, 2021

I might have a suggestion for a work-around for your problem. I'll get back to you.

Edit 1: Your solution works fine on UWP and WPF (iOS not tested). On android you can use the following workaround (not really a solution, more a proof of concept):

...

module App = 
    type Model = 
        { 
            Values: Tuple<string, string> list 
            LoadingView: bool
        }

    type Msg = 
        | Insert
        | Unload
        | Reset

    let initModel = 
        { 
            Values = [];
            LoadingView = false
        }

    let init () = initModel, Cmd.none

    let update msg model =
        match msg with
        | Insert ->
            // In reality these values come from a database and can change from time to time.
            // Sometimes there could be quite a lot of values, other times not so much.
            let values = [ ("Label 1", "Placeholder 1"); ("Label 2", "Placeholder 2"); ("Label 3", "Placeholder 3"); ("Label 4", "Placeholder 4") ]

            { model with 
                Values = values;
                LoadingView = true }, Cmd.ofMsg(Unload)      
        | Unload -> {model with LoadingView = false}, Cmd.none
        | Reset -> init ()

...


    let view (model: Model) dispatch =
        let standardView = 
            View.ScrollView(
                View.FlexLayout(
                    direction = FlexDirection.Column,
                    alignItems = FlexAlignItems.Center,
                    justifyContent = FlexJustify.SpaceEvenly,
                    children = [ 
                        // In reality, user input is more variable than just a button and this input will ultimately 
                        // decide whether labels, switches, entries, etc. are inserted.
                        View.Button(text = "Insert", horizontalOptions = LayoutOptions.Center, command = (fun () -> dispatch Insert))

                        View.Button(text = "Reset", horizontalOptions = LayoutOptions.Center, command = (fun () -> dispatch Reset))

                        match model.Values.Length with
                        | l when l > 0 ->
                            for value in model.Values do
                                insertView value
                        | _ -> ()
                        View.Label(text = "I'm near the bottom and may not be visible after you tap 'Insert'", horizontalOptions = LayoutOptions.Center, width=200.0, horizontalTextAlignment=TextAlignment.Center)
                    ]
                )
            )
        let loadingView = 
            View.ContentView(
                View.Label("loading")
            )
        View.ContentPage(
            content = 
                if (model.LoadingView = true)
                    then loadingView
                else standardView
        )

Basically, unloading and reloading the view fixes the scrolling problem.
If someone with a deeper understanding of the inner workings of fabulous / xamarin knows of a better way to 'reload' a view, I would be very interested.

Edit2:
a similar problem I encountered:
when switching from one carousel view to another, you'll encounter problems if the content of the carousel view is not identical (i.e. picture-picture-picture vs. picture-text-picture). However, if you load a different (not carousel) view in between, everything works fine.

Edit3 = some improvements to the code above

Edit 4: no scrolling problems with your original code on iOS either, seems to be an 'android only' problem.

@gaelian
Copy link
Author

gaelian commented Jul 7, 2021

Hey @reigam, I appreciate the help. Yeah, I've been mostly testing on Android and iOS (just Android for this issue so far) as these are the only relevant platforms for the project in question. I noticed that things seem to reappear when the views are updated again, subsequent to the addition of the child views on the previous update. I did end up using a CollectionView rather than ScrollView + FlexView. It doesn't work out exactly as I had envisioned in terms of layout appearance/behaviour, but will work fine.

After having sorted out overall scrolling for the page, I then realised I had a child ListView inside the parent CollectionView. But that's a different story.

@TimLariviere TimLariviere transferred this issue from fabulous-dev/Fabulous Jan 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants