I developed a custom navbar with subnav for one of my apps and had a couple of requests to share the details on the Slack group so here goes.
I used the BetterForms Components feature to build both elements. Let’s start with the Top Nav Bar.
TOP NAV BAR
This includes a breadcrumb element on the left, search field mid right and the user’s account menu on the right.
<div id="pageTop">
<div class="flex space-x-1 w-full bg-white px-4 md:pl-9 py-1">
<!--HYPERLINK BREADCRUMB-->
<div class="flex items-center content-center justify-start hidden-xs">
<div class="">
<!--SLOT 1-->
<button type="button" v-if="!schema.backText2" @click="namedAction(schema.backNamedActionName1)" :class="schema.styleClassesBackText1">
<i class="fas fa-chevron-left mr-3"></i>{{schema.backText1}}
</button>
<!--SLOT 1 & SLOT 2-->
<div v-if="schema.backText1 && schema.backText2 && !schema.backText3">
<button type="button" @click="namedAction(schema.backNamedActionName1)" :class="schema.styleClassesBackText1">
{{schema.backText1}}
</button>
<span class="mx-0 md:mx-4"><i class="fa-solid fa-chevron-right text-theme700"></i></span>
<button type="button" @click="namedAction(schema.backNamedActionName2)" :class="schema.styleClassesBackText2">
{{schema.backText2}}
</button>
</div>
<!--SLOT 1, SLOT 2 & SLOT 3-->
<div v-if="schema.backText1 && schema.backText2 && schema.backText3 && !schema.backText4">
<button type="button" @click="namedAction(schema.backNamedActionName1)" :class="schema.styleClassesBackText1">
{{schema.backText1}}
</button>
<span class="mx-0 md:mx-4"><i class="fa-solid fa-chevron-right text-theme700"></i></span>
<button type="button" @click="namedAction(schema.backNamedActionName2)" :class="schema.styleClassesBackText2">
{{schema.backText2}}
</button>
<span class="mx-0 md:mx-4"><i class="fa-solid fa-chevron-right text-theme700"></i></span>
<span :class="schema.styleClassesBackText3" class="font-light">{{schema.backText3}}</span>
</div>
<!--SLOT 1, SLOT 2, SLOT 3 & SLOT 4-->
<div v-if="schema.backText1 && schema.backText2 && schema.backText2 && schema.backText4">
<button type="button" @click="namedAction(schema.backNamedActionName1)" :class="schema.styleClassesBackText1">
{{schema.backText1}}
</button>
<span class="mx-0 md:mx-4"><i class="fa-solid fa-chevron-right text-theme700"></i></span>
<button type="button" @click="namedAction(schema.backNamedActionName2)" :class="schema.styleClassesBackText2">
{{schema.backText2}}
</button>
<span class="mx-0 md:mx-4"><i class="fa-solid fa-chevron-right text-theme700"></i></span>
<button type="button" @click="namedAction(schema.backNamedActionName3)" :class="schema.styleClassesBackText3">
{{schema.backText3}}
</button>
<span class="mx-0 md:mx-4"><i class="fa-solid fa-chevron-right text-theme700"></i></span>
<span :class="schema.styleClassesBackText4" class="font-light">{{schema.backText4}}</span>
</div>
</div>
</div>
<div class="hidden-sm hidden-md hidden-lg my-auto" @click="namedAction('toggleNavMobile',{})">
<i class="fa-duotone fa-bars fa-2x text-theme600"></i>
</div>
<!--SEARCH & ACCOUNT MENU-->
<div class="flex grow items-center content-center justify-end">
<div class="flex">
<!--Search Field-->
<div class="container-searchbar">
<input id="searchBar" class="searchbar" type="text" placeholder="Search" v-model="app.searchCriteria" @keyup.enter="namedAction('searchGlobal',{})" @keydown.enter.prevent onClick="this.select();"/>
<a id="btnSearch" class="btn-search"><i class="fal fa-search"></i></a>
</div>
<!--Account Menu-->
<div class="select-none mr-4">
<button @click="app.accountDropDown = !app.accountDropDown" type="button" class="py-4 text-black font-semibold items-center float-right" id="organizations-menu" aria-haspopup="true" aria-expanded="true">
<span v-if="app.organisations.length==1" class="hidden-xs hidden-sm">{{app.user.email}}</span>
<span v-else class="hidden-xs hidden-sm">{{app.activeOrganisation.company}}</span>
<span class="hidden-xs hidden-sm fa fa-chevron-down ml-2"></span>
<i class="inline hidden-md hidden-lg fa-duotone fa-sliders fa-2x text-theme600 mt-2"></i>
</button>
<button v-if="app.accountDropDown" @click="app.accountDropDown=false" class="fixed top-0 bottom-0 right-0 z-10 w-full h-full left-0 opacity-50 cursor-default">
</button>
<transition enter-active-class="transition ease-xout duration-100" enter-class="transform opacity-0 scale-95" enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75" leave-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
<div class="cursor-pointer origin-top-right absolute right-0 mt-12 mr-4 w-80 rounded-md shadow-lg" style="z-index: 20" v-if="app.accountDropDown">
<div class="rounded-md bg-white">
<div class="py-1 divide-y divide-gray-200" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
<ul class="mb-2 overflow-x-scroll" role="menu">
<li class="text py-2 px-3 font-semibold">
<h5 class="text-theme600">Organisation</h5>
</li>
<li class="px-3 hover:bg-gray-100 py-1" @click="namedAction('menuSelectOrganisation',{organisation: organisation})" v-for="organisation in app.organisations">
<a class="truncate">
<span class="icon">
<i class="fa w-5 text-theme600" :class="{'fa-check': app.activeOrganisation.id == organisation.idCOMPANY,}"></i>
</span>
<span :class="
{'font-semibold text-black':app.activeOrganisation.id == organisation.ididCOMPANY}">{{organisation.company}}</span>
</a>
</li>
</ul>
<ul class="" role="menu">
<li class=" px-3 hover:bg-gray-100 py-2" @click="app.accountDropDown=false;namedAction('menuSelectAccount',{})">
<a>
<span class="icon">
<i class="fa-duotone fa-user w-6 text-theme600"></i>
</span>Account</a>
</li>
</ul>
<ul class="" role="menu">
<li class=" px-3 hover:bg-gray-100 py-2" @click="app.accountDropDown=false;namedAction('menuSelectLogout',{})">
<a>
<span class="icon"><i class="fa-duotone fa-sign-out w-6 text-theme600"></i>
</span>Logout</a>
</li>
</ul>
</div>
</div>
</div>
</transition>
</div>
</div>
</div>
</div>
<!--Bottom Gray Border-->
<div class="flex h-2 bg-gradient-to-b from-gray-100 to-white"></div>
</div>
The JSON Element to add at the top of each page is as follows:
{
"_NOTE": "NAVBAR - BREADCRUMBS & SEARCH BAR",
"backNamedActionName1": "",
"backNamedActionName2": "",
"backNamedActionName3": "",
"backNamedActionName4": "",
"backText1_calc": "",
"backText2_calc": "",
"backText3_calc": "",
"backText4_calc": "",
"html": "",
"name": "Navbar",
"styleClassesBackText1": "breadcrumbLink",
"styleClassesBackText2": "breadcrumbLink",
"styleClassesBackText3": "breadcrumbLink",
"styleClassesBackText4": "breadcrumbLink",
"styleClassesSearchField": "fieldInput",
"type": "bfcomponent"
}
Breadcrumbs
The backText keys allow for dynamic setting of up to 4 levels of breadcrumb. My breadcrumbLink style is set in my app Settings Header using Tailwind so could could replace this styling with anything you want.
Search Field
This expands when the user clicks into the field which is accomplished with the following CSS added to the Site CSS. The searchGlobal namedAction runs when the user hits their enter key. I use this to search across a number of tables in my FileMaker file to build a series of arrays for different record types from my app and then navigate to a Results page which shows the matching records.
.container-searchbar {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.searchbar {
float: right;
background-color: #fff;
color: black;
padding: 6px 10px;
width: 80px;
border: none;
margin-top: 1px;
margin-right: 8px;
font-size: 1em;
font-weight: lighter;
border-top: none !important;
border-left: none !important;
border-right: none !important;
border-bottom: black dashed 1px !important;
border-radius: 0;
transition: 0.3s;
}
.searchbar::placeholder {
color: black;
font-size: 1em;
font-weight: normal;
/* transition: 0.2s; */
}
.searchbar:focus {
width: 300px;
font-size: 1em;
font-weight: normal;
transition: 0.3s;
/* Stops the input box from inheriting the styling from the inputs on the request form */
border: var(--sidebarBackgroundColorActive) solid 1px !important;
box-shadow: 0 0 0 2px #dce4fb;
border-radius: 3px;
outline: none;
}
.btn-search {
cursor: pointer;
color: black;
text-decoration: none !important;
/*font-family: "Segoe UI Light","Segoe UI","Segoe",Tahoma,Helvetica,Arial,sans-serif;*/
font-size: 1.5em;
font-weight: lighter;
padding-top: 5px;
margin-right: 40px;
}
Account
In my app users can be members of multiple organisations so I have a section that lists their organisations (which are saved in app) using a v-for and shows their active organisation (also saved in app) by matching app.organisation.id
with app.activeOrganisation.id
. If your users are only members of a single organisation you could remove the follow section of code.
<ul class="mb-2 overflow-x-scroll" role="menu">
<li class="text py-2 px-3 font-semibold">
<h5 class="text-theme600">Organisation</h5>
</li>
<li class="px-3 hover:bg-gray-100 py-1" @click="namedAction('menuSelectOrganisation',{organisation: organisation})" v-for="organisation in app.organisations">
<a class="truncate">
<span class="icon">
<i class="fa w-5 text-theme600" :class="{'fa-check': app.activeOrganisation.id == organisation.idCOMPANY,}"></i>
</span>
<span :class="{'font-semibold text-black':app.activeOrganisation.id == organisation.ididCOMPANY}">{{organisation.company}}
</span>
</a>
</li>
</ul>
SUB NAV BAR
I use the sub nav bar to display the page title and up to 4 buttons. Here is the component code
<div>
<div class="bg-white px-5 -mt-4 h-20 w-full flex justify-between">
<!--Record Details-->
<div class="flex items-center mx-4 invisible sm:visible">
<div v-if="schema.titleText" class="text-lg lg:text-2xl">
<span :class="schema.styleClassesGreenDot">
<i class="fa fa-circle fa-xs mr-3 text-green-400"></i>
</span>
<span class="font-semibold text-black">{{schema.titleText}}</span>
</div>
</div>
<!--Page Details-->
<div class="flex items-center mx-4">
<div v-if="schema.titleTextPage" class="mr-2 font-semibold text-black text-lg lg:text-xl">
{{schema.titleTextPage}}
</div>
</div>
<!--Buttons-->
<div class="flex items-center">
<button v-if="schema.btn4" class="mr-3" :class="schema.styleClassesBtn4" type="button" @click="namedAction(schema.namedActionBtn4)">
<i v-if="schema.btn4Icon" class="mr-2" :class="schema.btn4Icon"></i><span :class="schema.btn4TextStyle">{{schema.btn4}}</span>
</button>
<button v-if="schema.btn3 && !model.hideBtn3" class="mr-3" :class="schema.styleClassesBtn3" type="button" @click="namedAction(schema.namedActionBtn3)">
<i v-if="schema.btn3Icon" class="mr-2" :class="schema.btn3Icon"></i><span :class="schema.btn3TextStyle">{{schema.btn3}}</span>
</button>
<button v-if="schema.btn2 && !model.hideBtn2" class="mr-3" :class="schema.styleClassesBtn2" type="button" @click="namedAction(schema.namedActionBtn2)">
<i v-if="schema.btn2Icon" class="mr-2" :class="schema.btn2Icon"></i><span :class="schema.btn2TextStyle">{{schema.btn2}}</span>
</button>
<button v-if="schema.btn1" class="mr-5" :class="schema.styleClassesBtn1" type="button" @click="namedAction(schema.namedActionBtn1)">
<i v-if="schema.btn1Icon" class="mr-2" :class="schema.btn1Icon"></i><span :class="schema.btn1TextStyle">{{schema.btn1}}</span>
</button>
</div>
</div>
<!--Bottom Gray Border-->
<div :class="schema.styleClassesBottomBorder"></div>
</div>
and the JSON Element to add to your page
{
"_NOTE": "SUB NAVBAR - RECORD NAME AND BUTTONS",
"btn1Icon": "",
"btn1TextStyle": "",
"btn1": "",
"btn2Icon": "",
"btn2TextStyle": "",
"btn2": "",
"btn3Icon": "",
"btn3TextStyle": "",
"btn3": "",
"btn4Icon": "",
"btn4TextStyle": "",
"btn4": "",
"html": "",
"name": "SubNavbar",
"namedActionBtn1": "",
"namedActionBtn2": "",
"namedActionBtn3": "",
"namedActionBtn4": "",
"styleClassesBottomBorder": "flex h-2 bg-gradient-to-b from-gray-200 to-gray-100",
"styleClassesBtn1": "btnPrimary",
"styleClassesBtn2": "btnDelete",
"styleClassesBtn3": "btnSecondary",
"styleClassesBtn4": "btnSecondary",
"styleClassesGreenDot": "",
"titleText": "Neil Manchester",
"type": "bfcomponent"
}
Page Title
The page title on the left of the sub nav bar can either be hardcoded by setting titleText
or dynamically populated by adding _calc e.g. I use "titleText_calc": "moment(model.selectedTreatment.dateTreatment).format('DD/MM/YY') + ' - ' + model.selectedTreatment.patient",
to show the treatment date and name of the patient.
Buttons
You must include btn1, btn2, btn3 and btn4 as either empty strings or the wording of your button. Other empty keys can be deleted if not required. Adding _calc to either the btn key or btnXIcon keys means they can be set dynamically if required. Again, my button styleClasses use a bespoke class which is saved in my app header but you could use Tailwind here as I have with the styleClassesBottomBorder
I use FontAwesome for all of my app icons. These can also be set dynamically by adding _calc to the btnIcon keys. I use this to only show the icons on larger screen sizes e.g. "btn1Icon_calc": "[app.window.width > 700 ? 'fal fa-plus' : '']"
. app.window.width
is set when the user first logs into the app via the onAppLoad global namedAction.
As an additional feature I also made the 2nd and 3rd buttons dynamically visible so that I could choose to only display a Delete button if a record was selected to be deleted. This is achieved by setting "model.hideBtn2" : True
or "model.hideBtn3" : True
and then changing this to False
once a specific action has been completed, in my case records being selected.
The End Result
Here is a view of the final result. Works well and give a lot of flexibility. Don’t forget that in order to get this wokring you need to turn off the default BetterForms top nav which you can do in Styling - Theme Settings