-
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #64 from fabulous-dev/component-nav-sample
Navigation sample with components
- Loading branch information
Showing
35 changed files
with
926 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
namespace ComponentNavigation | ||
|
||
/// With each component runs in an isolated context, so we need a way to communicate between them. | ||
/// We define application-wide messages here, and a dispatcher to send and receive messages. | ||
[<RequireQualifiedAccess>] | ||
type AppMsg = | BackButtonPressed | ||
|
||
type IAppMessageDispatcher = | ||
abstract Dispatched: IEvent<AppMsg> | ||
abstract member Dispatch: AppMsg -> unit | ||
|
||
type AppMessageDispatcher() = | ||
let dispatched = Event<AppMsg>() | ||
|
||
interface IAppMessageDispatcher with | ||
member _.Dispatched = dispatched.Publish | ||
member _.Dispatch(msg) = dispatched.Trigger(msg) |
107 changes: 107 additions & 0 deletions
107
samples/Navigation/ComponentNavigation/ComponentNavigation.fsproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks> | ||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net8.0-windows10.0.19041.0</TargetFrameworks> | ||
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET --> | ||
<!-- <TargetFrameworks>$(TargetFrameworks);net7.0-tizen</TargetFrameworks> --> | ||
<OutputType>Exe</OutputType> | ||
<RootNamespace>ComponentNavigation</RootNamespace> | ||
<UseMaui>true</UseMaui> | ||
<SingleProject>true</SingleProject> | ||
<EnableDefaultItems>false</EnableDefaultItems> | ||
|
||
<!-- Display name --> | ||
<ApplicationTitle>ComponentNavigation</ApplicationTitle> | ||
|
||
<!-- App Identifier --> | ||
<ApplicationId>org.fabulous.maui.componentnavigation</ApplicationId> | ||
<ApplicationIdGuid>9abd223e-09e7-4649-b22b-7395cb4724e1</ApplicationIdGuid> | ||
|
||
<!-- Versions --> | ||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion> | ||
<ApplicationVersion>1</ApplicationVersion> | ||
|
||
<TargetPlatformIdentifier Condition=" $(TargetPlatformIdentifier) == '' ">$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))</TargetPlatformIdentifier> | ||
|
||
<SupportedOSPlatformVersion Condition="$(TargetPlatformIdentifier) == 'ios'">14.2</SupportedOSPlatformVersion> | ||
<SupportedOSPlatformVersion Condition="$(TargetPlatformIdentifier) == 'maccatalyst'">14.0</SupportedOSPlatformVersion> | ||
<SupportedOSPlatformVersion Condition="$(TargetPlatformIdentifier) == 'android'">21.0</SupportedOSPlatformVersion> | ||
<SupportedOSPlatformVersion Condition="$(TargetPlatformIdentifier) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion> | ||
<TargetPlatformMinVersion Condition="$(TargetPlatformIdentifier) == 'windows'">10.0.17763.0</TargetPlatformMinVersion> | ||
<SupportedOSPlatformVersion Condition="$(TargetPlatformIdentifier) == 'tizen'">6.5</SupportedOSPlatformVersion> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Compile Include="AppMsg.fs" /> | ||
<Compile Include="Navigation.fs" /> | ||
<Compile Include="PageA.fs" /> | ||
<Compile Include="PageB.fs" /> | ||
<Compile Include="PageC.fs" /> | ||
<Compile Include="Sample.fs" /> | ||
<Compile Include="MauiProgram.fs" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<!-- App Icon --> | ||
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" /> | ||
|
||
<!-- Custom Fonts --> | ||
<MauiFont Include="Resources\Fonts\*" /> | ||
|
||
<!-- Images --> | ||
<MauiImage Include="Resources\Images\*" /> | ||
<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208" /> | ||
|
||
<!-- Raw Assets (also remove the "Resources\Raw" prefix) --> | ||
<MauiAsset Include="Resources\Raw\*" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" /> | ||
|
||
<!-- Splash Screen --> | ||
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup Condition="$(TargetPlatformIdentifier) == 'android'"> | ||
<AndroidResource Include="$(AndroidProjectFolder)Resources/*/*" /> | ||
<AndroidResource Remove="$(AndroidProjectFolder)Resources/raw/.*" /> | ||
<AndroidResource Update="$(AndroidProjectFolder)Resources/raw/*" /> | ||
<AndroidAsset Include="$(AndroidProjectFolder)Assets/**/*" Exclude="$(AndroidProjectFolder)Assets/**/.*/**" /> | ||
<AndroidManifest Include="$(AndroidProjectFolder)AndroidManifest.xml" /> | ||
<Compile Include="$(AndroidProjectFolder)MainActivity.fs" /> | ||
<Compile Include="$(AndroidProjectFolder)MainApplication.fs" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup Condition="$(TargetPlatformIdentifier) == 'ios'"> | ||
<None Include="$(iOSProjectFolder)Info.plist" LogicalName="Info.plist" /> | ||
<Compile Include="$(iOSProjectFolder)AppDelegate.fs" /> | ||
<Compile Include="$(iOSProjectFolder)Program.fs" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup Condition="$(TargetPlatformIdentifier) == 'maccatalyst'"> | ||
<None Include="$(MacCatalystProjectFolder)Info.plist" LogicalName="Info.plist" /> | ||
<Compile Include="$(MacCatalystProjectFolder)AppDelegate.fs" /> | ||
<Compile Include="$(MacCatalystProjectFolder)Program.fs" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup Condition="$(TargetPlatformIdentifier) == 'windows'"> | ||
<Manifest Include="$(WindowsProjectFolder)app.manifest" /> | ||
<AppxManifest Include="$(WindowsProjectFolder)Package.appxmanifest" /> | ||
<Compile Include="$(WindowsProjectFolder)App.fs" /> | ||
<Compile Include="$(WindowsProjectFolder)Main.fs" /> | ||
|
||
<PackageReference Include="FSharp.Maui.WinUICompat" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup Condition="$(TargetPlatformIdentifier) == 'tizen'"> | ||
<TizenManifestFile Include="$(TizenProjectFolder)tizen-manifest.xml" /> | ||
<Compile Include="$(TizenProjectFolder)Main.fs" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="FSharp.Core" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\..\src\Fabulous.MauiControls\Fabulous.MauiControls.fsproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
namespace ComponentNavigation | ||
|
||
open Microsoft.Maui.Hosting | ||
open Fabulous.Maui | ||
|
||
type MauiProgram = | ||
static member CreateMauiApp() = | ||
let nav = NavigationController() | ||
let appMsgDispatcher = AppMessageDispatcher() | ||
|
||
MauiApp | ||
.CreateBuilder() | ||
.UseFabulousApp(Sample.view nav appMsgDispatcher) | ||
.ConfigureFonts(fun fonts -> | ||
fonts | ||
.AddFont("OpenSans-Regular.ttf", "OpenSansRegular") | ||
.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold") | ||
|> ignore) | ||
.Build() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
namespace ComponentNavigation | ||
|
||
open Fabulous | ||
|
||
/// This is the centerpiece of navigating through paths: | ||
/// A single enum regrouping all the navigation routes with their arguments | ||
[<RequireQualifiedAccess>] | ||
type NavigationRoute = | ||
| PageA | ||
| PageB of initialCount: int | ||
| PageC of someArgs: string * stepCount: int | ||
|
||
/// The NavigationController is used to notify the intention to navigate to a new page (or go back). | ||
/// We listen to it in a Cmd that will dispatch a message to the root of the application to trigger the actual navigation. | ||
type NavigationController() = | ||
let navigationRequested = Event<NavigationRoute>() | ||
let backNavigationRequested = Event<unit>() | ||
|
||
member this.NavigationRequested = navigationRequested.Publish | ||
member this.BackNavigationRequested = backNavigationRequested.Publish | ||
|
||
member this.NavigateTo(path: NavigationRoute) = navigationRequested.Trigger(path) | ||
|
||
member this.NavigateBack() = backNavigationRequested.Trigger() | ||
|
||
/// The Navigation module is a set of helper functions that will wrap the call to NavigationController into a Cmd. | ||
/// We do that because navigation is a side-effect and we want to keep it in a Cmd. | ||
module Navigation = | ||
let private navigateTo (nav: NavigationController) path : Cmd<'msg> = [ fun _ -> nav.NavigateTo(path) ] | ||
|
||
let navigateBack (nav: NavigationController) : Cmd<'msg> = [ fun _ -> nav.NavigateBack() ] | ||
|
||
let navigateToPageA nav = navigateTo nav NavigationRoute.PageA | ||
|
||
let navigateToPageB nav initialCount = | ||
navigateTo nav (NavigationRoute.PageB initialCount) | ||
|
||
let navigateToPageC nav someArgs stepCount = | ||
navigateTo nav (NavigationRoute.PageC(someArgs, stepCount)) | ||
|
||
/// The NavigationStack represents the history of the navigation. | ||
/// This is a simple stack of pages that the app will use to remember and display the pages needed. | ||
type NavigationStack = | ||
{ BackStack: NavigationRoute list | ||
CurrentPage: NavigationRoute | ||
ForwardStack: NavigationRoute list } | ||
|
||
static member Init(path: NavigationRoute) = | ||
{ BackStack = [] | ||
CurrentPage = path | ||
ForwardStack = [] } | ||
|
||
member this.Push(path: NavigationRoute) = | ||
{ BackStack = this.CurrentPage :: this.BackStack | ||
CurrentPage = path | ||
ForwardStack = [] } | ||
|
||
member this.Pop() = | ||
match this.BackStack with | ||
| [] -> this | ||
| head :: tail -> | ||
{ BackStack = tail | ||
CurrentPage = head | ||
ForwardStack = [] } | ||
|
||
member this.UpdateCurrentPage(path: NavigationRoute) = { this with CurrentPage = path } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
namespace ComponentNavigation | ||
|
||
open Fabulous | ||
open Fabulous.Maui | ||
|
||
open type Fabulous.Maui.View | ||
|
||
/// Each page is "isolated". They have their own MVU loop and own types. | ||
/// The only dependency they receive from outside is the NavigationController, which is passed to the update function. | ||
module PageA = | ||
type Model = { IsActive: bool; Count: int } | ||
|
||
type Msg = | ||
| Active | ||
| Inactive | ||
| Increment | ||
| Decrement | ||
| GoBack | ||
| GoToPageB | ||
| GoToPageC | ||
| BackButtonPressed | ||
|
||
/// Since the NavigationRoute.PageA doesn't take arguments, the init function excepts a unit parameter. | ||
let init () = | ||
{ IsActive = false; Count = 0 }, Cmd.none | ||
|
||
let update (nav: NavigationController) msg model = | ||
match msg with | ||
| Active -> { model with IsActive = true }, Cmd.none | ||
| Inactive -> { model with IsActive = false }, Cmd.none | ||
| Increment -> { model with Count = model.Count + 1 }, Cmd.none | ||
| Decrement -> { model with Count = model.Count - 1 }, Cmd.none | ||
| GoBack -> model, Navigation.navigateBack nav | ||
| GoToPageB -> model, Navigation.navigateToPageB nav model.Count | ||
| GoToPageC -> model, Navigation.navigateToPageC nav "Hello from Page A!" model.Count | ||
| BackButtonPressed -> { model with Count = model.Count - 1 }, Cmd.none | ||
|
||
let subscribe (appMsgDispatcher: IAppMessageDispatcher) model = | ||
let localAppMsgSub dispatch = | ||
appMsgDispatcher.Dispatched.Subscribe(fun msg -> | ||
match msg with | ||
| AppMsg.BackButtonPressed -> dispatch BackButtonPressed) | ||
|
||
[ if model.IsActive then | ||
[ nameof localAppMsgSub ], localAppMsgSub ] | ||
|
||
let program nav appMsgDispatcher = | ||
Program.statefulWithCmd init (update nav) | ||
|> Program.withSubscription(subscribe appMsgDispatcher) | ||
|
||
let view nav appMsgDispatcher = | ||
Component(program nav appMsgDispatcher) { | ||
let! model = Mvu.State | ||
|
||
ContentPage( | ||
Grid(coldefs = [ Star ], rowdefs = [ Star; Auto ]) { | ||
VStack() { | ||
Label($"Count: {model.Count}").centerTextHorizontal() | ||
|
||
Button("Increment", Increment) | ||
Button("Decrement", Decrement) | ||
} | ||
|
||
(VStack() { | ||
Button("Go back", GoBack) | ||
Button("Go to Page B", GoToPageB) | ||
Button("Go to Page C", GoToPageC) | ||
}) | ||
.gridRow(1) | ||
} | ||
) | ||
.title("Page A") | ||
.onNavigatedTo(Active) | ||
.onNavigatedFrom(Inactive) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
namespace ComponentNavigation | ||
|
||
open Fabulous | ||
open Fabulous.Maui | ||
|
||
open type Fabulous.Maui.View | ||
|
||
module PageB = | ||
type Model = | ||
{ IsActive: bool | ||
InitialCount: int | ||
Count: int } | ||
|
||
type Msg = | ||
| Active | ||
| Inactive | ||
| Increment | ||
| Decrement | ||
| GoBack | ||
| GoToPageA | ||
| GoToPageC | ||
|
||
/// Contrary to PageA, NavigationPath.PageB has a initialCount argument so the init function will receive it. | ||
let init initialCount = | ||
{ IsActive = false | ||
InitialCount = initialCount | ||
Count = initialCount }, | ||
Cmd.none | ||
|
||
let update (nav: NavigationController) msg model = | ||
match msg with | ||
| Active -> { model with IsActive = true }, Cmd.none | ||
| Inactive -> { model with IsActive = false }, Cmd.none | ||
| Increment -> { model with Count = model.Count + 1 }, Cmd.none | ||
| Decrement -> { model with Count = model.Count - 1 }, Cmd.none | ||
| GoBack -> model, Navigation.navigateBack nav | ||
| GoToPageA -> model, Navigation.navigateToPageA nav | ||
| GoToPageC -> model, Navigation.navigateToPageC nav "Hello from Page A!" model.Count | ||
|
||
let subscribe (appMsgDispatcher: IAppMessageDispatcher) model = | ||
let localAppMsgSub dispatch = | ||
appMsgDispatcher.Dispatched.Subscribe(fun msg -> | ||
match msg with | ||
| AppMsg.BackButtonPressed -> dispatch GoBack) | ||
|
||
[ if model.IsActive then | ||
[ nameof localAppMsgSub ], localAppMsgSub ] | ||
|
||
let program nav appMsgDispatcher = | ||
Program.statefulWithCmd init (update nav) | ||
|> Program.withSubscription(subscribe appMsgDispatcher) | ||
|
||
let view nav appMsgDispatcher arg = | ||
Component(program nav appMsgDispatcher, arg) { | ||
let! model = Mvu.State | ||
|
||
ContentPage( | ||
Grid(coldefs = [ Star ], rowdefs = [ Star; Auto ]) { | ||
VStack() { | ||
Label($"Initial count: {model.InitialCount}") | ||
|
||
Label($"Count: {model.Count}").centerTextHorizontal() | ||
|
||
Button("Increment", Increment) | ||
Button("Decrement", Decrement) | ||
} | ||
|
||
(VStack() { | ||
Button("Go back", GoBack) | ||
Button("Go to Page A", GoToPageA) | ||
Button("Go to Page C", GoToPageC) | ||
}) | ||
.gridRow(1) | ||
} | ||
) | ||
.title("Page B") | ||
.onNavigatedTo(Active) | ||
.onNavigatedFrom(Inactive) | ||
} |
Oops, something went wrong.