By Daniel Wood, 26 May 2022
In Part One of our Navigation series we talked about how to implement navigation within a single screen by building a portal of Navigation records. This utilised a table called NavigationItems where each record was an element in the navigation portal - be it a screen, a header, or a separator.
In this article we extend this table further to build a Navigation Bar used for solution-wide navigation. We introduce a new NavigationSets table and talk about it's purpose for allowing multiple different types of Navigation Bars.
We also talk a bit about Dashboards and how these can be customised to suit each individual user within a Navigation Bar.
So, let's jump right in!
This is the type of article where it will really help if you follow along using the example file because a lot of the examples given have come direct from our example file.
A Navigation Bar is a horizontal (or vertical) object that contains buttons the user can interact with the navigate to different screens within a solution. Below is an example of the Navigation Bar you'll find in our example file:
They can be built in a number of ways using different FileMaker techniques.
Historically at Digital Fusion our Navigation Bar for years was a series of repeating fields placed across the top of each layout. We would have a repeating field for an icon, another for the label, and more for the buttons that the user interacts with.
One of the disadvantages we had with this approach was that our Navigation Bars were context dependent. Every time we wished to place it on a new layout of a different context we would have to re-point all the fields to the parent context. The fact that it was comprised of multiple fields and objects also made it a pain to work with.
These days we have abandoned repeating fields in favour of utilising button bars in conjunction with JSON. We construct a JSON object which is a complete representation of each Navigation Bar, and then load the JSON into a global variable for the user. This is used by the Navigation Bar for both its display, and for determining which action is to be carried out when a button is pressed.
With the information stored within a JSON object we have everything we need to build our Navigation Bar object. However that is only half the story. The basis of the JSON is a couple of navigation based tables. Think of these as our underlying navigational structure. We have already seen one of these tables in our first article - NavigationItems - and now we'll talk a bit more about the structure for our Navigation Bar.
The NavigationItems table stores information about each individual item in our navigation object. In part one we saw it used for items in a portal based control we built for navigation within a single screen. We are now going to use it as a store of information about each item represented in our Navigation Bar.
The majority of fields we utilised for the portal navigation will be reused for our navigation bar. The main exception however is the icon. Because we will be using a button bar object, we have no control over dynamic display. Any icons in a button bar would have to be hard-coded into each individual segment, and this defeats our goal of building a Navigation Bar that is entirely soft-coded. For this reason we will not be using icons.
We also won't be using separators or titles in our Navigation Bar - each item in the button bar will be clickable.
We have however introduced two new fields into our table:
The first is id_dashboard. This will serve as a foreign key, pointing to another special NavigationItem record. We'll talk more about how this works later.
The second is id_navigationSet. This is a foreign key also and links each NavigationItem record to a parent NavigationSet record.
Think of this as a parent table for a collection of NavigationItems. Each record in this table serves as a single Navigation Bar.
The table comprises of the fields:
That's about it, not much to it really.
If you want 10 different navigation bars in your solution then you would create 10 different records in this table each with its own set of NavigationItems. Once you have your Navigation Sets created with items, then it becomes a case of deciding which one to use for any given user.
In part one we saw 3 different types of NavigationItem records. We had one for a screen, a separator, or a header. These were defined using the type field.
However in our Navigation Bar implementation we won't be using types. All NavigationItem records will represent an item in the Navigation Bar that can be clicked upon to initiate an action.
We do however have one special type of NavigationItem record which we call the Dashboard record
We always assign a value of "0" for the sort field on this special record, and this is how we identify the dashboard record from all other NavigationItem records. This is because our Dashboard navigation will always be the left-most button in our navigation bar - no exceptions.
The other difference between this record and other NavigationItem records is that on this record the only other field value we need provide is that of id_dashboard. As mentioned earlier this is a foreign key to a dashboard record. We are effectively telling the NavigationSet what the default dashboard screen will be for this navigation bar.
The key here is default. The Dashboard is able to be changed on any given navigation bar when initialised based on whatever criteria you wish. Some systems assign dashboards to specific users, while others assign based on the role a user might be undertaking.
We want our navigation to be flexible such that we can easily change what happens when this left-most button to access the dashboard is clicked.
A dashboard record is just a NavigationItem record whose type field value is equal to "dashboard". They have no direct relationship to any specific NavigationSet.
You create one record for each different dashboard in your solution. These are configured in the same way as a normal NavigationItem record where you specify:
You don't need to define a sort order for these records - when they are linked up to a NavigationSet they'll always have a sort order of 0.
Recall earlier we created a special NavigationItem record in each NavigationSet to act as the dashboard position with a sort order of 0.
We said that each item only needs to be related to a dashboard record. Now that we have set up a number of dashboard records as shown above, we can now set the id_dashboard field value our dashboard NavigationItems records.
Next step is to configure your regular NavigationItem records in your Navigation Set. These are set up in a very similar way to the NavigationItems we saw in part one for our portal based navigation.
One difference here from part one is that our target will be a layout name instead of layout object name. We also have introduced an Alternative layout name as an option. This is such that we can hold a modifier key while pressing a button in our Navigation Bar to access a secondary layout. It is useful where you may have both a form and list view version of a screen and wish for a quick and simple way for a user to choose which of the two varieties they want to navigate to.
Lastly we have an option to run a script as part of the button click. You can either use this on its own or in conjunction with a specified layout. If you specify both then the script will run after the layout is navigated to.
At this point if you're following along we should have the following records defined in order to build a Navigation Bar:
With all this information we have everything we need to now construct a JSON representation of a NavigationSet.
The JSON object will contain the following:
Here is an example of a basic navigation Set with a dashboard and 2 screens:
The JSON is constructed using a script called "NAV: Initialise". This script is responsible for taking a NavigationSet record as its parameter and constructing the JSON representation.
Now, we won't go into fine detail about each line of code in this script because you'll find it well commented in our example file to dissect, however let's give a high level overview of what it does...
1. Determine Navigation Bar
While a NavigationSet id can be passed to this script, if the user does not specify one then there are some fall-backs. The first is trying to use the value in the global variable $$USER_NAVIGATION_ID.
Now, this is purely an arbitrary name and depends on your own solution, but effectively think of this as a NavigationSet that has been assigned to the logged in user. This can sometimes be done on startup of a solution or when a user defines their role, or some other criteria.
Whatever the case may be, it may transpire users have their own Navigation Bars, and in our example that ID gets stored in this global variable.
Now, failing there being one, the script will do a second fall-back to look for the system default Navigation Set. If no default has been defined, then we are unable to proceed.
2. Obtain NavigationItem data.
We next run an ExecuteSQL query to obtain the required information from each NavigationItem record associated with the NavigationSet. We then separate the left-most (dashboard) entry from the rest (regular items).
3. Determine Dashboard to use.
We handle the dashboard item differently because within this item is the id_dashboard value which we now need to look up to obtain the required information from this Dashboard NavigationItem.
There is also a part in the script here where you have the ability to determine a user-specific overriding dashboard. Remember we're dealing up to this point with the default Dashboard for the Navigation Set. However in your own implementation you may wish to define dashboards at the user level meaning dashboard records are actually assigned to individual users. If this is the case then this part of the script is where you would check if the user has a specified dashboard, and override the dashboard record to use with the user specified one.
We again run an executeSQL query to now obtain the required information from the chosen Dashboard NavigationItem.
4. Build the JSON
The last part is pretty simple, we now just construct the JSON and build the array of items. The first position in the array is always for that of the dashboard, with the remainder of the items going into the subsequent array positions.
The actual loading is a piece of cake and is a single line of code. We simply set the resulting JSON into the $$USER_NAVIGATION_DATA global variable. Everything else is now handled by the Navigation Bar object, and the script that runs when an item is clicked.
To change the navigation bar for a user, just simply re-run the Nav:Initialise script, passing it the id of a new NavigationSet.
The Navigation Bar as previously mentioned is a button bar object. Each element is soft-coded based on the JSON in $$USER_NAVIGATION_DATA.
Taking an initial look we can learn the following:
Firstly let's look at the calculation for each regular item in the nav bar:
@NAV_Get ( "name" ; 1 )
This is a custom function which we use in our implementation to help obscure some of the more lengthy calculations and simply allow us to query our JSON for a specific value based on item index. In this case we're asking for the name value for the item at array position 1. This is what we use to show the name of the second item in the navigation bar (remember arrays in JSON begin at index 0).
Each item has an action assigned which will run the Nav:Select script. This script accepts the following parameter:
JSONSetElement ( "" ; "target_number" ; 1 ; JSONNumber )
Which is just a JSON object containing the index of the item.
We want to use the active button bar state to indicate which layout the user is currently on. To do this we first give each segment an object name containing its index, e.g.:
Next, we define the active segment to be:
"nav_" & @NAV_Get ( "Position" ; "" )
You'll see we have again utilised our custom function, but this time we are passing it a special parameter "position". We are requesting the function to tell us at which position in the list of items our current layout name resides. The function will check each navigation Item for both its primary layout and alternate layout name, returning the first index position in the array it finds.
In the example pictured above, if we were on the highlighted layout then this custom function call would return "2" and thus the active segment object name would be "nav_2".
Again there is nothing really technical here. This script accepts as parameter an index position. It then looks up our JSON objects array of Navigation Items for the corresponding position, from which it can then obtain the layout name, alternate layout name and script. It then is responsible for carrying out the correct navigation and execution of a nominated script.
Unlike portal based navigation that can be scrolled, we do not have that luxury with a horizontal Navigation Bar. Now, to be honest if you are building Navigation Bars where you want to employ horizontal scrolling then you're doing it wrong - you have too many items in your nav bar and need to rethink your solutions Navigation.
However being the hypocrites we are, we added into our Navigation Bar object the means to allow a small number of additional screens that will open in a vertical popover if there are more than can be displayed horizontally.
Here you'll see the last item in the bar is a special item labelled "More…" which is a popover, containing further Navigation Bar items. These additional items are basically set up in the same way as the horizontal items.
In our solution setup we first define a special global variable $$SOLUTION_NAV_SIZE. This is set to a number which is how many actual items we can display horizontally. In the case of our example file this number is 8 (we exclude the "more" segment).
In situations where our Navigation Bar has 8 or less items we will hide this "More…" popover, and simply show our 8 regular segments.
However if we have over 8 items in the Navigation Bar then we hide the last regular segment and instead replace it with the "More…" popover. Thus the last segment in the Navigation Bar will always either be the last normal item or the "More…" popover.
Inside the popover our segments will begin at position 8. This is because in order to show the "More…" popover, we hid the 8th segment in the horizontal view, and so we now require it to be the staring segment in the vertical view.
Because you cannot use the same layout object name twice, these vertical object names are suffixed with "_alt" to differentiate them from their horizontal counterparts. We just make a small adjustment to the active segment calculation here to accommodate.
As always we have crafted an example file to help you follow along with the article and so you can explore and implement this in your own solutions.