In this blog post, I will break out the components associated with the Header in Power Apps Portals.
Problem Statement
How to customize header in a Power Apps Portals? How to hide/show navigations in the Header displayed on Power Apps Portals? How to update options in the header menu in a Power Apps Portal Application? How to disable search in Power Apps Portals? How does login and login out work in a Power Apps Portal Application?
Solution
The header is present OOTB as part of a web template when you provision a Power Apps Portal Application.
To locate Header Web Template:
1) Open the Portal Management app from make.powerapps.com
2) Scroll and select Web Templates from the Site Map.
3) Select the Header Web Template and open it to view the code behind it.
In this case, we are using "Custom Portal" template for Power Apps Portals. Below are the different components present in the Web Template.
Component #1: Set the local language code and Home Page URL.
{% assign defaultlang = settings['LanguageLocale/Code'] | default: 'en-us' %}
{% assign homeurl = website.adx_partialurl %}
Here, we have defined a variable defaultlang which fetches the local language stored in the Site settings, but if it is empty then will set English-US as the default language.
Secondly, it is fetching partial URL of the website and , which takes the users to Home Page (which is the root page and has a partial URL of /).
Component #2: Sets the Header when the device size is extra small (<768px).
<div class="visible-xs-block">
{% editable snippets 'Mobile Header' type: 'html' %}
</div>
Here, a content snippet named 'Mobile Header' is referenced to show the content. In the template that I am using, it is just showing a text, but if you want to add an image, that can be added in the Mobile Header content snippet.
Component #3: Sets the Header when the device size is small, medium, or large (>768px).
<div class="visible-sm-block visible-md-block visible-lg-block navbar-brand">
{% editable snippets 'Navbar Left' type: 'html' %}
</div>
Here, a content snippet named 'Navbar Left' is referenced to show the content. In the template that I am using, it is just showing a text, but if you want to add an image, that can be added in the Navbar Left content snippet.
Component #4: This is the hamberger menu which is displayed on the mobile/tablet screen devices (< 1200px).
<button type="button" class="navbar-toggle collapsed" title="{{ snippets["Header/Toggle Navigation"] | default: resx['Toggle_Navigation'] | h }}" data-toggle="collapse" data-target="#navbar" aria-expanded="false" onclick="setHeight();">
<span class="sr-only">{{ snippets["Header/Toggle Navigation"] | default: resx['Toggle_Navigation'] | h }}</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
Here, it is using 'Header/Toggle Navigation' content snippet to set the title of button control.
Component #5: Fetches the web links present in 'Primary Navigation' web link set to show the navigations on the header.
{% assign primary_nav = weblinks["Primary Navigation"] %}
Note: If you wish to add/remove links from this header, you need to update the Primary Navigation web link set on the Portal Management app. If you wish to use a different web link set, then you can update the name in above snippet.
Component #6: This part of the webtemplate is rendering the navigations including search and login functionality.
{% if primary_nav %}
<div class="navbar-right menu-bar {% if primary_nav.editable %}xrm-entity xrm-editable-adx_weblinkset{% endif %}" data-weblinks-maxdepth="2">
<ul class="nav navbar-nav weblinks" role="menubar">
...
</ul>
{% editable primary_nav %}
</div>
{% endif %}
Here, it is creating an unordered list to show the links present in the Primary Navigation web link set. This is a bigger component, so let's break it into a few sub-components.
SubComponent #6.1:
{% for link in primary_nav.weblinks %}
This is iterating over all the links present inside the Web link set.
SubComponent #6.2:
{% unless forloop.first %}
<li class="divider-vertical" aria-hidden="true"></li>
{% endunless %}
This creates a divider between the links unless the current item is the first link. The means, it will add the divider between the first and second link and follow it on all the links later.
SubComponent #6.3:
{% if link.display_page_child_links %}
{% if link.url != null %}
{% assign sublinks = sitemap[link.url].children %}
{% endif %}
{% else %}
{% assign sublinks = link.weblinks %}
{% endif %}
Here, it assigns sublinks of the associated web link in case of any child items.
SubComponent #6.4:
<li role="none" class="weblink {% if sublinks.size > 0 %} dropdown{% endif %}">
<a role="menuitem"
aria-label="{{ link.name | escape }}"
{% if sublinks.size > 0 -%}
href="#" class="dropdown-toggle" data-toggle="dropdown"
{%- else -%}
href="{{ link.url | escape }}"
{%- endif -%}
{%- if link.Open_In_New_Window %} target="_blank" {% endif -%}
{%- if link.nofollow %} rel="nofollow"{% endif -%}
{%- if link.tooltip %} title="{{ link.tooltip | escape }}"{% endif %}>
{%- if link.image -%}
{%- if link.image.url startswith '.' -%}
<span class="{{ link.image.url | split:'.' | join }}" aria-hidden="true"></span>
{%- else -%}
<img src="{{ link.image.url | escape }}"
alt="{{ link.image.alternate_text | default:link.tooltip | escape }}"
{% if link.image.width %}width="{{ link.image.width | escape }}" {% endif %}
{% if link.image.height %}height="{{ link.image.height | escape }}" {% endif %} />
{%- endif -%}
{%- endif -%}
{%- unless link.display_image_only -%}
{{ link.name | escape }}
{%- endunless -%}
{%- if sublinks.size > 0 -%}
<span class="caret"></span>
{%- endif -%}
</a>
{% if sublinks.size > 0 %}
<ul class="dropdown-menu" role="menu">
{% if link.url %}
<li role="none">
<a role="menuitem"
aria-label="{{ link.name | escape }}"
href="{{ link.url }}"
{% if link.Open_In_New_Window %} target="_blank" {% endif %}
{% if link.nofollow %}rel="nofollow" {% endif %}
{% if link.tooltip %}title="{{ link.tooltip | escape }}" {% endif %}>{{ link.name | escape }}</a>
</li>
<li class="divider"></li>
{% endif %}
{% for sublink in sublinks %}
<li role="none">
<a role="menuitem"
aria-label="{{ sublink.name | default:sublink.title | escape }}"
href="{{ sublink.url }}"
{% if sublink.Open_In_New_Window %} target="_blank" {% endif %}
{% if sublink.nofollow %}rel="nofollow" {% endif %}
{% if sublink.tooltip %}title="{{ sublink.tooltip | escape }}" {% endif %}>
{{ sublink.name | default:sublink.title | escape }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
It created a list item in the unordered list created in Component #6.
a) Created an anchor for the individual items. If the link has sublinks present, then it will add the dropdown class which expands to show the sub links.
b) It adds different attributes provided on the Web Link Set to specify the Title, URL, Tooltip, target, and Image (whichever attribute is applied).
c) In case of any sublinks, it repeats the process to create an unordered list in the dropdown for the user to display. (Sublink area code is underlined above.)
Component #7: Enables search functionality on the header after the navigation is created.
{% assign search_enabled = settings['Search/Enabled'] | boolean | default:true %}
{% if search_enabled %}
<li class="divider-vertical" aria-hidden="true"></li>
<li class="dropdown" role="none">
<a id="search" class="navbar-icon" href="#" data-toggle="dropdown"
role="button" aria-haspopup="true" aria-expanded="false"
aria-label="{{ snippets["Header/Search/ToolTip"] | default:resx["Search_DefaultText"] | escape }}" >
<span class="glyphicon glyphicon-search"></a>
</a>
<ul class="dropdown-menu dropdown-search">
<li>
{% include 'Search' %}
</li>
</ul>
</li>
{% endif %}
Here, it checks if the search is enabled in Search Settings, if it is, then it will display the search icon, otherwise it will hide it from the header.
Note: If you want to hide the search, then you can set the 'Search/Enabled' Site Setting to false.
Component #8: Created the language dropdown if multiple languages are enabled.
{% if website.languages.size > 1 %}
<li class="dropdown" role="none">
<a class="dropdown-toggle" href="#" data-toggle="dropdown" role="menuitem" aria-label="{{ website.selected_language.name | escape }}" aria-haspopup="true" aria-expanded="false" title="{{ website.selected_language.name | escape }}">
<span class="drop_language">{{ website.selected_language.name | escape }}</span>
<span class="caret"></span>
</a>
{% include 'Languages Dropdown' %}
</li>
<li class="divider-vertical" aria-hidden="true"></li>
{% endif %}
Here, it checks the number of languages associated with the current website and then displays a dropdown with all the languages if more than one language is associated.
Note: It is referring to Languages Dropdown, which is iterating over all the languages available for the current page and showing a dropdown to switch if needed.
Component #9: Displays Login/Logout functionality with navigation to Profile Page.
{% if user %}
<li class="dropdown" role="none">
<a href="#" class="dropdown-toggle" title="{{ user.fullname | escape }}" data-toggle="dropdown" role="menuitem" aria-haspopup="true" aria-expanded="false">
<span class="username">{{ user.fullname | escape }}</span>
<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
{% assign show_profile_nav = settings["Header/ShowAllProfileNavigationLinks"] | boolean | default:true %}
{% if show_profile_nav %}
{% assign profile_nav = weblinks["Profile Navigation"] %}
{% if profile_nav %}
{% for link in profile_nav.weblinks %}
<li role="none">
<a role="menuitem" aria-label="{{ link.name | escape }}" href="{{ link.url | escape }}" title="{{ link.name | escape }}">{{ link.name | escape }}</a>
</li>
{% endfor %}
{% endif %}
{% else %}
<li role="none"><a role="menuitem" aria-label="{{ snippets["Profile Link Text"] | default:resx["Profile_Text"] | escape }}" href="{{ sitemarkers['Profile'].url | escape }}">{{ snippets["Profile Link Text"] | default:resx["Profile_Text"] | escape }}</a></li>
{% endif %}
<li class="divider" role="separator" aria-hidden="true"></li>
<li role="none">
<a role="menuitem" aria-label="{{ snippets["links/logout"] | default:resx["Sign_Out"] | escape }}" href="{% if homeurl%}/{{ homeurl }}{% endif %}{{ website.sign_out_url_substitution }}" title="{{ snippets["links/logout"] | default:resx["Sign_Out"] | escape }}">
{{ snippets["links/logout"] | default:resx["Sign_Out"] | escape }}
</a>
</li>
</ul>
</li>
{% else %}
<li role="none">
<a role="menuitem" aria-label="{{ snippets["links/login"] | default:resx["Sign_In"] | escape }}" href="{% if homeurl%}/{{ homeurl }}{% endif %}{{ website.sign_in_url_substitution }}">
{{ snippets["links/login"] | default:resx["Sign_In"] | escape }}
</a>
</li>
{% endif %}
Here, it checks if the User context is available, this means if the user is logged in, it will display a dropdown with an option to navigate to Profile and Logout. If the user is not logged in, then it displayed only the Login option.
Note: If you want to add/remove navigations in the dropdown, you can update "Profile Navigation" web link set.
Component #10:
<div class="navbar-right hidden-xs">
{% editable snippets 'Navbar Right' type: 'html' %}
</div>
Here, it associates the Navbar Right content snippet. By default, this is empty but if you want, this can be edited in the Portal Management App.
Component #11:
{% substitution %}
{% assign current_page = page.id %}
{% assign sr_page = sitemarkers["Search"].id %}
{% assign forum_page = sitemarkers["Forums"].id %}
{% if current_page %}
{% if current_page == sr_page or current_page == forum_page %}
{% assign section_class = "section-landing-search" %}
{% if current_page == forum_page %}
{% assign section_class = "section-landing-forums" %}
{% endif %}
<section class="page_section {{ section_class | h }} color-inverse">
<div class="container">
<div class="row ">
<div class="col-md-12 text-center">
{% if current_page == sr_page %}
<h1 class="section-landing-heading">{% editable snippets 'Search/Title' default: resx["Discover_Contoso"] %}</h1>
{% include 'Search' %}
{% endif %}
</div>
</div>
</div>
</section>
{% endif %}
{% endif %}
{% endsubstitution %}
This section is used to avoid caching issues for the pages, this tag provides the content block in the header or footer where the output of the wrapped content block doesn't get cached.
Component #12:
<script type="text/javascript">
window.onload = function() {
if(window.navigator.appName == "Microsoft Internet Explorer" || window.navigator.userAgent.indexOf("Trident") > 0){
var searchElement = document.getElementById("search");
if (searchElement != null) searchElement.setAttribute("href", "");
}
};
function setHeight(){
var windowHeight = window.innerHeight - 140;
var navbar = document.getElementById("navbar");
if (navbar) {
navbar.style.maxHeight = windowHeight + "px";
}
}
window.addEventListener('resize', function (event) {
setHeight();
});
</script>
This script is loaded when the page is loaded, it enables Search Element to work by setting up the href attribute.
Also, this sets the height of the navigation window which expands on a mobile device to properly occupy the space based on the screen size.
The whole template can be found here:
{% assign defaultlang = settings['LanguageLocale/Code'] | default: 'en-us' %}
{% assign homeurl = website.adx_partialurl %}
<div class="navbar navbar-inverse navbar-static-top" role="navigation">
<div class="container">
<div class="navbar-header">
<div class="visible-xs-block">
{% editable snippets 'Mobile Header' type: 'html' %}
</div>
<div class="visible-sm-block visible-md-block visible-lg-block navbar-brand">
{% editable snippets 'Navbar Left' type: 'html' %}
</div>
<button type="button" class="navbar-toggle collapsed" title="{{ snippets["Header/Toggle Navigation"] | default: resx['Toggle_Navigation'] | h }}" data-toggle="collapse" data-target="#navbar" aria-expanded="false" onclick="setHeight();">
<span class="sr-only">{{ snippets["Header/Toggle Navigation"] | default: resx['Toggle_Navigation'] | h }}</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<div id="navbar" class="navbar-collapse collapse">
{% assign primary_nav = weblinks["Primary Navigation"] %}
{% if primary_nav %}
<div class="navbar-right menu-bar {% if primary_nav.editable %}xrm-entity xrm-editable-adx_weblinkset{% endif %}" data-weblinks-maxdepth="2">
<ul class="nav navbar-nav weblinks" role="menubar">
{% for link in primary_nav.weblinks %}
{% unless forloop.first %}
<li class="divider-vertical" aria-hidden="true"></li>
{% endunless %}
{% if link.display_page_child_links %}
{% if link.url != null %}
{% assign sublinks = sitemap[link.url].children %}
{% endif %}
{% else %}
{% assign sublinks = link.weblinks %}
{% endif %}
<li role="none" class="weblink {% if sublinks.size > 0 %} dropdown{% endif %}">
<a role="menuitem"
aria-label="{{ link.name | escape }}"
{% if sublinks.size > 0 -%}
href="#" class="dropdown-toggle" data-toggle="dropdown"
{%- else -%}
href="{{ link.url | escape }}"
{%- endif -%}
{%- if link.Open_In_New_Window %} target="_blank" {% endif -%}
{%- if link.nofollow %} rel="nofollow"{% endif -%}
{%- if link.tooltip %} title="{{ link.tooltip | escape }}"{% endif %}>
{%- if link.image -%}
{%- if link.image.url startswith '.' -%}
<span class="{{ link.image.url | split:'.' | join }}" aria-hidden="true"></span>
{%- else -%}
<img src="{{ link.image.url | escape }}"
alt="{{ link.image.alternate_text | default:link.tooltip | escape }}"
{% if link.image.width %}width="{{ link.image.width | escape }}" {% endif %}
{% if link.image.height %}height="{{ link.image.height | escape }}" {% endif %} />
{%- endif -%}
{%- endif -%}
{%- unless link.display_image_only -%}
{{ link.name | escape }}
{%- endunless -%}
{%- if sublinks.size > 0 -%}
<span class="caret"></span>
{%- endif -%}
</a>
{% if sublinks.size > 0 %}
<ul class="dropdown-menu" role="menu">
{% if link.url %}
<li role="none">
<a role="menuitem"
aria-label="{{ link.name | escape }}"
href="{{ link.url }}"
{% if link.Open_In_New_Window %} target="_blank" {% endif %}
{% if link.nofollow %}rel="nofollow" {% endif %}
{% if link.tooltip %}title="{{ link.tooltip | escape }}" {% endif %}>{{ link.name | escape }}</a>
</li>
<li class="divider"></li>
{% endif %}
{% for sublink in sublinks %}
<li role="none">
<a role="menuitem"
aria-label="{{ sublink.name | default:sublink.title | escape }}"
href="{{ sublink.url }}"
{% if sublink.Open_In_New_Window %} target="_blank" {% endif %}
{% if sublink.nofollow %}rel="nofollow" {% endif %}
{% if sublink.tooltip %}title="{{ sublink.tooltip | escape }}" {% endif %}>
{{ sublink.name | default:sublink.title | escape }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
{% assign search_enabled = settings['Search/Enabled'] | boolean | default:true %}
{% if search_enabled %}
<li class="divider-vertical" aria-hidden="true"></li>
<li class="dropdown" role="none">
<a id="search" class="navbar-icon" href="#" data-toggle="dropdown"
role="button" aria-haspopup="true" aria-expanded="false"
aria-label="{{ snippets["Header/Search/ToolTip"] | default:resx["Search_DefaultText"] | escape }}" >
<span class="glyphicon glyphicon-search"></a>
</a>
<ul class="dropdown-menu dropdown-search">
<li>
{% include 'Search' %}
</li>
</ul>
</li>
{% endif %}
<li class="divider-vertical" aria-hidden="true"></li>
{% if website.languages.size > 1 %}
<li class="dropdown" role="none">
<a class="dropdown-toggle" href="#" data-toggle="dropdown" role="menuitem" aria-label="{{ website.selected_language.name | escape }}" aria-haspopup="true" aria-expanded="false" title="{{ website.selected_language.name | escape }}">
<span class="drop_language">{{ website.selected_language.name | escape }}</span>
<span class="caret"></span>
</a>
{% include 'Languages Dropdown' %}
</li>
<li class="divider-vertical" aria-hidden="true"></li>
{% endif %}
{% if user %}
<li class="dropdown" role="none">
<a href="#" class="dropdown-toggle" title="{{ user.fullname | escape }}" data-toggle="dropdown" role="menuitem" aria-haspopup="true" aria-expanded="false">
<span class="username">{{ user.fullname | escape }}</span>
<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
{% assign show_profile_nav = settings["Header/ShowAllProfileNavigationLinks"] | boolean | default:true %}
{% if show_profile_nav %}
{% assign profile_nav = weblinks["Profile Navigation"] %}
{% if profile_nav %}
{% for link in profile_nav.weblinks %}
<li role="none">
<a role="menuitem" aria-label="{{ link.name | escape }}" href="{{ link.url | escape }}" title="{{ link.name | escape }}">{{ link.name | escape }}</a>
</li>
{% endfor %}
{% endif %}
{% else %}
<li role="none"><a role="menuitem" aria-label="{{ snippets["Profile Link Text"] | default:resx["Profile_Text"] | escape }}" href="{{ sitemarkers['Profile'].url | escape }}">{{ snippets["Profile Link Text"] | default:resx["Profile_Text"] | escape }}</a></li>
{% endif %}
<li class="divider" role="separator" aria-hidden="true"></li>
<li role="none">
<a role="menuitem" aria-label="{{ snippets["links/logout"] | default:resx["Sign_Out"] | escape }}" href="{% if homeurl%}/{{ homeurl }}{% endif %}{{ website.sign_out_url_substitution }}" title="{{ snippets["links/logout"] | default:resx["Sign_Out"] | escape }}">
{{ snippets["links/logout"] | default:resx["Sign_Out"] | escape }}
</a>
</li>
</ul>
</li>
{% else %}
<li role="none">
<a role="menuitem" aria-label="{{ snippets["links/login"] | default:resx["Sign_In"] | escape }}" href="{% if homeurl%}/{{ homeurl }}{% endif %}{{ website.sign_in_url_substitution }}">
{{ snippets["links/login"] | default:resx["Sign_In"] | escape }}
</a>
</li>
{% endif %}
</ul>
{% editable primary_nav %}
</div>
{% endif %}
<div class="navbar-right hidden-xs">
{% editable snippets 'Navbar Right' type: 'html' %}
</div>
</div>
</div>
</div>
{% substitution %}
{% assign current_page = page.id %}
{% assign sr_page = sitemarkers["Search"].id %}
{% assign forum_page = sitemarkers["Forums"].id %}
{% if current_page %}
{% if current_page == sr_page or current_page == forum_page %}
{% assign section_class = "section-landing-search" %}
{% if current_page == forum_page %}
{% assign section_class = "section-landing-forums" %}
{% endif %}
<section class="page_section {{ section_class | h }} color-inverse">
<div class="container">
<div class="row ">
<div class="col-md-12 text-center">
{% if current_page == sr_page %}
<h1 class="section-landing-heading">{% editable snippets 'Search/Title' default: resx["Discover_Contoso"] %}</h1>
{% include 'Search' %}
{% endif %}
</div>
</div>
</div>
</section>
{% endif %}
{% endif %}
{% endsubstitution %}
<script type="text/javascript">
window.onload = function() {
if(window.navigator.appName == "Microsoft Internet Explorer" || window.navigator.userAgent.indexOf("Trident") > 0){
var searchElement = document.getElementById("search");
if (searchElement != null) searchElement.setAttribute("href", "");
}
};
function setHeight(){
var windowHeight = window.innerHeight - 140;
var navbar = document.getElementById("navbar");
if (navbar) {
navbar.style.maxHeight = windowHeight + "px";
}
}
window.addEventListener('resize', function (event) {
setHeight();
});
</script>
You can reuse this web template to update the design as per the requirement.
In this post, we saw how to customize the header of a Power Apps Portals application. This can be helpful in cases when the requirement is to build the application based on branding guidelines.
I hope this was useful to you. In case of any questions or suggestions, feel free to reach out on Twitter at @agarwal_ritika.