Navigation
This tutorial gives a high level overview of how Fuse's main navigation system works, using classes like Router
, Navigator
and PageControl
.
Complete app example
This example app show how to use the navigation system in Fuse:
Creating pages
When designing a multi-page app, it is recommended to define each page in its own UX file as an ux:Class
, like this:
LoginPage.ux
<Page ux:Class="LoginPage">
<Router ux:Dependency="router" />
<JavaScript File="LoginPage.js" />
...
</Page>
SettingsPage.ux
<Page ux:Class="SettingsPage">
<Router ux:Dependency="router" />
<JavaScript File="SettingsPage.js" />
...
</Page>
And so on, for each page.
Notice how each page can have its own <JavaScript>
tag to handle internal logic for that page.
Most pages will need access to the app's Router
, so we declare that as an ux:Dependency="router"
. This means the JavaScript on that page can access that router.
If our page has other dependencies, we can declare them the same way.
Assembling the App
The basic structure of your main UX file should be something like this:
<App>
<Router ux:Name="router" />
<Navigator DefaultPath="login">
<LoginPage ux:Template="login" router="router" />
<HomePage ux:Template="home" router="router" />
<SettingsPage ux:Template="settings" router="router" />
<UserProfilePage ux:Template="user" router="router" />
</Navigator>
</App>
The ux:Template
attribute indicates that this is not a real object instance, but rather a template that can be instantiated on demand. The Navigator
will take care of instantiating as many instances of each template as needed, and recycle instances when they are not needed anymore.
The DefaultPath
specifies what templates should be instantiated by default when the app starts up and no route is set yet.
Notice how we inject the dependencies just like properties. It is good practice to keep names of dependencies lowercase, like names. This allows you to extract components without renaming your objects, and makes it easy to spot the difference between a dependency and a property.
Note that all dependencies must be specified, otherwise you'll get a compile time error.
The pages do not need to be descendants of the Page class. Any visual object, such as a Panel can be used.
Navigating around (flat navigation)
In the above example, the login
template (LoginPage
class) will be our starting page.
Let's give the LoginPage
a button:
<Button Text="Login" Clicked="{loginClicked}" />
Hook its Clicked
event up to this function in LoginPage.js
:
function loginClicked() {
// TODO: validate login credentials
router.goto("home");
}
This will do what you expect, the navigator will animate the login screen out, and bam! We're on the home
page.
Remember to add
loginClicked
to yourmodule.exports
.
Hierarchical navigation
Hierarchical navigation is navigating to ("pushing") a temporary page, and then later "popping" (going back) to wherever you came from, either using an on-screen button, or the device's physical back-button.
Let's say a button is supposed to open the settings
page, and then the back-button should take us back to wherever we came from:
router.push("settings");
Easy, right? Now the back button on Android will take us back.
In local preview, use Cmd/Ctrl+B to emulate the back button. On iOS, hold 3 fingers for 1 second to bring up a menu where we can emulate the back button.
We can also trigger a go-back from JavaScript:
router.goBack();
We can use push()
multiple times to navigate "deeper". Then you have to tap the back button multiple times to get all the way back out.
We can use goto()
while inside a deep stack of pushed pages to discard the stack and simply go to that page. Using the back button after that will not take you back.
Passing parameters to pages
Sometimes you want to use the same page template, but open it with different parameters.
For example, our user
page could really benefit from knowing the ID of the user to display the profile for.
This is easy by passing argument objects to the router:
router.push("user", { userId: id });
This will temporarily open a new instance of our user
template, with the provided object as parameter.
In UserPage.js
we can read the parameter using the Observable Parameter
, which lets you react to changes to the parameters:
var userId = this.Parameter.map(function(param) {
return param.userId;
});
module.exports = {
userId: userId
};
Make a note of the fact that this.Parameter
is an Observable, which means that its value is not necessarily available when the module is being evaluated.
Make sure you either expose it using an observable operator (like in the example above), or add a handler to it using the onValueChanged
function:
this.Parameter.onValueChanged(module, function(param) {
//At this point we know then new parameter value.
//module is used to connect the lifetime of the
//subscription to the lifetime of the module
});
Note that this
in the root scope of UserPage.js
refers to the current instance of the UserPage
class.
Be aware that in JavaScript, the meaning of the
this
keyword varies with function scope. Make sure you grab the reference to the right instance in the root of your module.
Note that the same UserPage
instance may be reused by the Navigator
to display different users over time. Parameter
will only be updated when you need to display a new user.
Multi-level navigation
Sometimes, it is not enough to just have one level of navigation. Our home
screen, for example, may contain multiple pages within itself.
No worries, the Router
can deal with this just as easily. Say that our HomePage.ux
looks something like this:
<Page ux:Class="HomePage">
<Router ux:Dependency="router" />
<JavaScript File="HomePage.js" />
<PageControl>
<SocialFeedPage ux:Name="socialfeed" />
<DirectChatsPage ux:Name="chats" />
<PinnedMessagesPage ux:Name="pinned" />
<RecentNotificationsPage ux:Name="recent" />
</PageControl>
</Page>
Note: PageControl expects to get live page objects, not templates. PageControl keeps all pages alive and only one instance of each, to e.g. allow swipe navigation between them.
Both Navigator
and PageControl
are so-called router outlets. This means they participate in routing. When using .goto()
or .push()
, we can provide a multi-level route, with parameters for each level.
For example, our login-screen might want to send us directly to the RecentNotificationsPage
instead of SocialFeedPage
. That's done like this:
router.goto("home", {}, "recent", { scrollOffset: 300 });
And that's all! First the Navigator
will get you to the home
template, and then the PageControl
within that template will go to the recent
page. This is a path, and can also be specified relatively, as we will see below. This can be compared to the path of an URL, which would look like this: home/recent?scrollOffset=300
The home
template in this case does not take any parameters, so we pass it an empty object ({}
).
Meanwhile, the recent
template will get the parameter { scrollOffset: 300 }
object to its Parameter
Observable, as an example of how we can pass parameters to pages.
Relative navigation
A neat trick to making custom pages independent of a large navigation tree, is to navigate relative to the closest Navigator object, instead of the whole tree. This is done using the gotoRelative
and pushRelative
functions.
These two functions behave in the same way as their other variants(goto
, and push
), except that they expect a Navigator
or PageControl
as the first argument. Any path specified after this will be relative to the provided Navigator/PageControl.
<JavaScript>
module.exports.toSub1 = function() {
router.pushRelative(secondNav, "option1");
}
</JavaScript>
<Navigator ux:Name="secondNav" DefaultTemplate="optionList">
<Panel ux:Template="optionList">
<Button Margin="10" Text="Subpage 1" Clicked="{toSub1}" />
</Panel>
<Panel Color="#FAA" ux:Template="option1" />
</Navigator>
Custom animations/transitions
Note that by default Page
objects have a built-in default Transition="Default"
, which means the containing control defines a meaningful default transition. For example, PageControl
by default makes pages slide horizontally to enter and exit, while
in Navigator
enter sliding in from the left, and exit shrinking and fading out into the background.
If you want to customize the transtion, you probably want to disable the default transition first:
<Page ux:Class="SettingsPage" Transition="None">
If you don't specify Transition="None"
, your custom animation will be "on top of"/"in addition to" the default transition.
The Navigator
works with the standard navigation animation system in Fuse. This means that page transitions are defined by the following triggers:
EnteringAnimation
- played backwards for the entering page (the page becoming the active page)ExitingAnimation
- played forwards for pages replaced by apush
RemovingAnimation
- played forwards for pages removed because of agoto
When you goBack()
, the opposite happens`:
EnteringAnimation
- played forwards for the page being removed by agoBack
ExitingAnimation
- played backwards for the page being restored bt agoBack
This system can be a bit tricky to wrap your head around, but it is actually necessary to allow you to fully customize all the four different cases.
For example:
<Page ux:Class="SettingsPage" Transition="None">
<EnteringAnimation>
<Move X="1" RelativeTo="Size" Duration="0.4" Easing="BackOut" />
</EnteringAnimation>
<ExitingAnimation>
<Move Y="1" RelativeTo="Size" Duration="0.4" Easing="CubicIn" />
</ExitingAnimation>
...
</Page>
This will make your settings page fly in from the left, and exit by falling out the bottom of your screen. Feel free
to spice it up by adding <Change this.Opacity="0" Duration="0.3" ../>
, scaling, rotation or whatever you feel like.
If you have a lot of pages and want them to share the transition code, we can make a common base class:
<Page ux:Class="FancyTransitionPage" Transition="None">
<EnteringAnimation>
<Move X="1" RelativeTo="Size" Duration="0.4" Easing="BackOut" />
</EnteringAnimation>
<ExitingAnimation>
<Move Y="1" RelativeTo="Size" Duration="0.4" Easing="CubicIn" />
</ExitingAnimation>
</Page>
And then use that as base class for your pages:
<FancyTransitionPage ux:Class="SettingsPage">
...
</FancyTransitionPage>
Other navigation-based triggers such as ActivatingAnimation
, WhileActive
etc. also works as expected in both PageControl
and Navigator
.
And done!
That's it, you now have an elegant, scalable model for navigation and structuring your app.