Mega Menu with SPFx Application Customizer

**Update: Added on GitHub**

https://github.com/nannerup/spfx-megamenu

My main critique point of the Office 365 modern experience has always been the lack of ability to customize. This is such an important factor when working with corporate customers.

Luckily this has somewhat improved with the GA of SPFx a year ago. You’ve got to work with what you’ve got, and this blog post shows how to implement a responsive mega menu with an SPFx Application Customizer and the SharePoint Term Store.

This enables actual global navigation across team sites and communication sites, which can be handled in one central spot.

Let’s dig in!

Create SPFx Application Customizer

It is assumed, that the whole SPFx toolchain is available – if not: https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-development-environment

Create a new SPFx Application Customizer using the yoman generator:

> What is your solution name? MegaMenu
> Which baseline packages do you want to target for your component(s)? SharePoint Online only (latest)
> Where do you want to place the files? Use the current folder
> Do you want to allow the tenant admin the choice of being able to deploy 
  the solution to all sites immediately without running any feature deployment or adding apps in sites? Yes
> Which type of client-side component to create? Extension
> Which type of client-side extension to create? Application Customizer
> What is your Application Customizer name? MegaMenu
> What is your Application Customizer description? MegaMenu description

code .

The “code .” will start Visual Studio Code, and open our news generated Application Customizer.

Additional Files

In the folder “src/extensions/megaMenu” you must add two additional files:

  1. MegaMenu.module.scss
  2. services/SPTermStoreService.ts (Create the folder “services”)

MegaMenu.module.scss

.app {
    .menuContainer, .menuContainer * {
        box-sizing: border-box;
      }
      
      a {
        color: #333;
      }
      
      
      
      /* ––––––––––––––––––––––––––––––––––––––––––––––––––
      megamenu.js STYLE STARTS HERE
      –––––––––––––––––––––––––––––––––––––––––––––––––– */
      
      
      /* ––––––––––––––––––––––––––––––––––––––––––––––––––
      Screen style's
      –––––––––––––––––––––––––––––––––––––––––––––––––– */
      
      .menuContainer {
        width: 100%;
        margin: 0 auto;
        background: #e9e9e9;
      }
      
      .menuMobile {
        display: none;
        padding: 20px;
        &:after {
          content: "\EF66";
          font-family: "FabricMDL2Icons";
          font-size: 1.5rem;
          line-height: 2.5rem;
          padding: 0;
          float: right;
          position: relative;
          top: 50%;
          transform: translateY(-25%);
        }
      }
      
      .menuDropdownIcon {
        &:before {
          content: "\E710";
          font-family: "FabricMDL2Icons";
          display: none;
          cursor: pointer;
          float: right;
          padding: 1.5em 2em;
          background: #fff;
          color: #333;
        }
      }

      .menu > ul {
        margin: 0 auto;
        width: 100%;
        list-style: none;
        padding: 0;
        position: relative;
        //position: relative;
        /* IF .menu position=relative -> ul = container width, ELSE ul = 100% width */
        -webkit-box-sizing: border-box;
        -moz-box-sizing: border-box;
        box-sizing: border-box;
      }

      .menu > ul:before,
      .menu > ul:after{
        content: "";
        display: table;
      } 

      .menu > ul:after{
        clear: both;
      }

      .menu > ul > li{
        float: left;
        background: #e9e9e9;
        padding: 0;
        margin: 0;
      }

      .menu > ul > li a{
        text-decoration: none;
        padding: 0.75em 2em;
        display: block;
      }

      .menu > ul > li a:hover{
        background: #f0f0f0;
      }

      .menu > ul > li > ul{
        display: none;
        width: 100%;
        background: #f0f0f0;
        padding: 20px;
        position: absolute;
        z-index: 99;
        left: 0;
        margin: 0;
        list-style: none;
        -webkit-box-sizing: border-box;
        -moz-box-sizing: border-box;
        box-sizing: border-box;
      }

      .menu > ul > li > ul:before,
      .menu > ul > li > ul:after{
        content: "";
        display: table;
      }

      .menu > ul > li > ul:after{
        clear: both;
      }

      .menu > ul > li > ul > li{
        margin: 0;
        padding-bottom: 0;
        list-style: none;
        width: 25%;
        background: none;
        float: left;
      }

      .menu > ul > li > ul > li a{
        color: #777;
        padding: .2em 0;
        width: 95%;
        display: block;
        border-bottom: 1px solid #ccc;
      }

      .menu > ul > li > ul > li > ul{
        display: block;
        padding: 0;
        margin: 10px 0 0;
        list-style: none;
        -webkit-box-sizing: border-box;
        -moz-box-sizing: border-box;
        box-sizing: border-box;
      }

      .menu > ul > li > ul > li > ul:before,
      .menu > ul > li > ul > li > ul:after{
        content: "";
        display: table;
      }

      .menu > ul > li > ul > li > ul:after{
        clear: both;
      }

      .menu > ul > li > ul > li > ul > li{
        float: left;
        width: 100%;
        padding: 10px 0;
        margin: 0;
        font-size: .8em;
      }

      .menu > ul > li > ul > li > ul > li a{
        border: 0;
      }

      .menu > ul > li > ul.normal-sub{
        width: 300px;
        left: auto;
        padding: 10px 20px;
      }

      .menu > ul > li > ul.normal-sub > li{
        width: 100%;
      }

      .menu > ul > li > ul.normal-sub > li a{
        border: 0;
        padding: 1em 0;
      }
            
      
      /* ––––––––––––––––––––––––––––––––––––––––––––––––––
      Mobile style's
      –––––––––––––––––––––––––––––––––––––––––––––––––– */
      
      @media only screen and (max-width: 959px) {
        .menuContainer {
          width: 100%;
        }
        .menuMobile {
          display: block;
        }
        .menuDropdownIcon {
          &:before {
            display: block;
          }
        }
        .menu {
          > ul {
            display: none;
            > li {
              width: 100%;
              float: none;
              display: block;
              a {
                padding: 1.5em;
                width: 100%;
                display: block;
              }
              > ul {
                position: relative;
                &.normalSub {
                  width: 100%;
                }
                > li {
                  float: none;
                  width: 100%;
                  margin-top: 20px;
                  &:first-child {
                    margin: 0;
                  }
                  > ul {
                    position: relative;
                    > li {
                      float: none;
                    }
                  }
                }
              }
            }
          }
          .showOnMobile {
            display: block;
          }
        }
      }
}

The styling of the mega menu has been brutally borrowed from: https://codepen.io/riogrande/pen/MKXweV

SPTermStoreService.ts

import { IWebPartContext} from '@microsoft/sp-webpart-base';
import { Environment, EnvironmentType } from '@microsoft/sp-core-library';
import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions } from '@microsoft/sp-http';

/**
 * @interface
 * Interface for SPTermStoreService configuration
 */
export interface ISPTermStoreServiceConfiguration {
  spHttpClient: SPHttpClient;
  siteAbsoluteUrl: string;
}


/**
 * @interface
 * Generic Term Object (abstract interface)
 */
export interface ISPTermObject {
  identity: string;
  isAvailableForTagging: boolean;
  name: string;
  guid: string;
  customSortOrder: string;
  terms: ISPTermObject[];
  localCustomProperties: any;
}

/**
 * @class
 * Service implementation to manage term stores in SharePoint
 * Basic implementation taken from: https://oliviercc.github.io/sp-client-custom-fields/
 */
export class SPTermStoreService {

  private spHttpClient: SPHttpClient;
  private siteAbsoluteUrl: string;
  private formDigest: string;

  /**
   * @function
   * Service constructor
   */
  constructor(config: ISPTermStoreServiceConfiguration){
      this.spHttpClient = config.spHttpClient;
      this.siteAbsoluteUrl = config.siteAbsoluteUrl;
  }

  /**
   * @function
   * Gets the collection of term stores in the current SharePoint env
   */
  public async getTermsFromTermSetAsync(termSetName: string, termSetLocal: Number): Promise<ISPTermObject[]> {
    if (Environment.type === EnvironmentType.SharePoint ||
        Environment.type === EnvironmentType.ClassicSharePoint) {

      //First gets the FORM DIGEST VALUE
      let contextInfoUrl: string = this.siteAbsoluteUrl + "/_api/contextinfo";
      let httpPostOptions: ISPHttpClientOptions = {
        headers: {
          "accept": "application/json",
          "content-type": "application/json"
        }
      };
      let response: SPHttpClientResponse = await this.spHttpClient.post(contextInfoUrl, SPHttpClient.configurations.v1, httpPostOptions);
      let jsonResponse: any = await response.json();
      this.formDigest = jsonResponse.FormDigestValue;

      //Build the Client Service Request
      let clientServiceUrl = this.siteAbsoluteUrl + '/_vti_bin/client.svc/ProcessQuery';
      let data = '<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="JavaScript Client" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="2" ObjectPathId="1" /><ObjectIdentityQuery Id="3" ObjectPathId="1" /><ObjectPath Id="5" ObjectPathId="4" /><ObjectIdentityQuery Id="6" ObjectPathId="4" /><ObjectPath Id="8" ObjectPathId="7" /><Query Id="9" ObjectPathId="7"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="Terms" SelectAll="true"><Query SelectAllProperties="false"><Properties /></Query></Property></Properties></ChildItemQuery></Query></Actions><ObjectPaths><StaticMethod Id="1" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="4" ParentId="1" Name="GetDefaultSiteCollectionTermStore" /><Method Id="7" ParentId="4" Name="GetTermSetsByName"><Parameters><Parameter Type="String">' + termSetName + '</Parameter><Parameter Type="Int32">' + termSetLocal + '</Parameter></Parameters></Method></ObjectPaths></Request>';
      httpPostOptions = {
        headers: {
          'accept': 'application/json',
          'content-type': 'application/json',
          "X-RequestDigest": this.formDigest
        },
        body: data
      };

      let serviceResponse: SPHttpClientResponse = await this.spHttpClient.post(clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions);
      let serviceJSONResponse: Array<any> = await serviceResponse.json();

      let result: Array<ISPTermObject> = new Array<ISPTermObject>();

      // Extract the object of type SP.Taxonomy.TermSetCollection from the array
      let termSetsCollections = serviceJSONResponse.filter(
        (child: any) => (child != null && child['_ObjectType_'] !== undefined && child['_ObjectType_'] === "SP.Taxonomy.TermSetCollection")
      );

      // And if any, process the TermSet objects in it
      if (termSetsCollections != null && termSetsCollections.length > 0) {
        let termSetCollection = termSetsCollections[0];

        let childTermSets = termSetCollection['_Child_Items_'];

        // Extract the object of type SP.Taxonomy.TermSet from the array
        let termSets = childTermSets.filter(
          (child: any) => (child != null && child['_ObjectType_'] !== undefined && child['_ObjectType_'] === "SP.Taxonomy.TermSet")
        );

        // And if any, process the requested TermSet object
        if (termSets != null && termSets.length > 0) {
          let termSet = termSets[0];

          let termsCollection = termSet['Terms'];
          let childItems = termsCollection['_Child_Items_'];

          return(await Promise.all<ISPTermObject>(childItems.map(async (t: any) : Promise<ISPTermObject> => {
            return await this.projectTermAsync(t);
          })));
        }
      }
    }

    // Default empty array in case of any missing data
    return (new Promise<Array<ISPTermObject>>((resolve, reject) => {
      resolve(new Array<ISPTermObject>());
    }));
  }


  /**
   * @function
   * Gets the child terms of another term of the Term Store in the current SharePoint env
   */
  private async getChildTermsAsync(term: any): Promise<ISPTermObject[]> {

    // Check if there are child terms to search for
    if (Number(term['TermsCount']) > 0) {

      //Build the Client Service Request
      let clientServiceUrl = this.siteAbsoluteUrl + '/_vti_bin/client.svc/ProcessQuery';
      let data = '<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName=".NET Library" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="20" ObjectPathId="19" /><Query Id="21" ObjectPathId="19"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="true"><Properties><Property Name="CustomSortOrder" ScalarProperty="true" /><Property Name="LocalCustomProperties" ScalarProperty="true" /></Properties></ChildItemQuery></Query></Actions><ObjectPaths><Property Id="19" ParentId="16" Name="Terms" /><Identity Id="16" Name="' + term['_ObjectIdentity_'] + '" /></ObjectPaths></Request>';
      let httpPostOptions: ISPHttpClientOptions = {
        headers: {
          'accept': 'application/json',
          'content-type': 'application/json',
          "X-RequestDigest": this.formDigest
        },
        body: data
      };
      let serviceResponse: SPHttpClientResponse = await this.spHttpClient.post(clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions);
      let serviceJSONResponse: Array<any> = await serviceResponse.json();

      // Extract the object of type SP.Taxonomy.TermCollection from the array
      let termsCollections = serviceJSONResponse.filter(
        (child: any) => (child != null && child['_ObjectType_'] !== undefined && child['_ObjectType_'] === "SP.Taxonomy.TermCollection")
      );

      // And if any, get the first and unique Terms collection object
      if (termsCollections != null && termsCollections.length > 0) {
        let termsCollection = termsCollections[0];

        let childItems = termsCollection['_Child_Items_'];

        return(await Promise.all<ISPTermObject>(childItems.map(async (t: any) : Promise<ISPTermObject> => {
          return await this.projectTermAsync(t);
        })));
      }
    }

    // Default empty array in case of any missing data
    return (new Promise<Array<ISPTermObject>>((resolve, reject) => {
      resolve(new Array<ISPTermObject>());
    }));
  }

  /**
   * @function
   * Projects a Term object into an object of type ISPTermObject, including child terms
   * @param guid
   */
  private async projectTermAsync(term: any) : Promise<ISPTermObject> {

    return({
      identity: term['_ObjectIdentity_'] !== undefined ? term['_ObjectIdentity_'] : "",
      isAvailableForTagging: term['IsAvailableForTagging'] !== undefined ? term['IsAvailableForTagging'] : false,
      guid: term['Id'] !== undefined ? this.cleanGuid(term['Id']) : "",
      name: term['Name'] !== undefined ? term['Name'] : "",
      customSortOrder: term['CustomSortOrder'] !== undefined ? term['CustomSortOrder'] : "",
      terms: await this.getChildTermsAsync(term),
      localCustomProperties: term['LocalCustomProperties'] !== undefined ? term['LocalCustomProperties'] : null,
    });
  }

  /**
   * @function
   * Clean the Guid from the Web Service response
   * @param guid
   */
  private cleanGuid(guid: string): string {
    if (guid !== undefined)
      return guid.replace('/Guid(', '').replace('/', '').replace(')', '');
    else
      return '';
  }
}

The SP Term Store Service is the same as used on SPFx’s own GitHub repo: https://github.com/SharePoint/sp-dev-fx-extensions/tree/master/samples

Additional Components

Now move over to Application Customizer’s main file, MegaMenuApplicationCustomizer.ts. Here we need to do some modifications.

First of all the we need to extend the import from the SP Application Base, to be able to use the placeholders on the page.

Secondly, we need to import some additional components:

  • Our newly added Styles
  • The SP Term Store Service
  • jQuery
  • The SP PnP JS Library

The beginning of the MegaMenuApplicationCustomizer.ts should like this:

import { override } from '@microsoft/decorators';
import { Log } from '@microsoft/sp-core-library';
import {
  BaseApplicationCustomizer,
  PlaceholderContent,
  PlaceholderName
} from '@microsoft/sp-application-base';
import { Dialog } from '@microsoft/sp-dialog';

import * as strings from 'MegaMenu2ApplicationCustomizerStrings';

import styles from './MegaMenu.module.scss';
import * as SPTermStore from './services/SPTermStoreService';
import pnp from "sp-pnp-js";
import * as jQuery from 'jquery';
const NAV_TERMS_KEY: string = 'global-navigation-terms';

const LOG_SOURCE: string = 'MegaMenu2ApplicationCustomizer';

Visual Studio Code will now most likely show some errors on jQuery and the PnP JS Library, as the modules are not to be found.

These can be added using Node.js Command Prompt (libraries will be downloaded and added):

npm install --save jquery@2
npm install --save sp-pnp-js

Now we need to modify the interface:

export interface IMegaMenuApplicationCustomizerProperties {
  // This is an example; replace with your own property
  TopMenuTermSet?: string;
}

Mega Menu

Now that the basics are in place, we need to add the mega menu specific code to our Application Customizer.

First of all we need to modify our onInit method (don’t forget the global variables _topPlaceholder and _topMenuItems):

private _topPlaceholder: PlaceholderContent | undefined;
private _topMenuItems: SPTermStore.ISPTermObject[];
  
@override
  public async onInit(): Promise<void> {
    Log.info(LOG_SOURCE, `Initialized ${strings.Title}`);

    // Added to handle possible changes on the existence of placeholders.
    this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);

    // Configure caching
    pnp.setup({
      defaultCachingStore: "session",
      defaultCachingTimeoutSeconds: 900, //15min
      globalCacheDisable: true // true to disable caching in case of debugging/testing
    });

    // Retrieve the menu items from taxonomy
    let termStoreService: SPTermStore.SPTermStoreService = new SPTermStore.SPTermStoreService({
      spHttpClient: this.context.spHttpClient,
      siteAbsoluteUrl: this.context.pageContext.web.absoluteUrl,
    });

    if (this.properties.TopMenuTermSet != null) {
      let cachedTerms = pnp.storage.session.get(NAV_TERMS_KEY);
      if (cachedTerms != null) {
        //Use cached terms
        this._topMenuItems = cachedTerms;
        
      }
      else {
        this._topMenuItems = await termStoreService.getTermsFromTermSetAsync(this.properties.TopMenuTermSet, this.context.pageContext.web.language);
        //Store in cache
        pnp.storage.session.put(NAV_TERMS_KEY, this._topMenuItems);
        
      }

      //Generate Mega Menu HTML
      let menuString: string = this.generateMegaMenuLevel(this._topMenuItems);

      //Set HTML
      jQuery("#menu ul").html(menuString);
    }

    // Call render method for generating the HTML elements.
    this._renderPlaceHolders();



    jQuery('#menu > ul > li:has( > ul)').addClass(`${styles.menuDropdownIcon}`);
    //Checks if li has sub (ul) and adds class for toggle icon - just an UI

    jQuery('#menu > ul > li > ul:not(:has(ul))').addClass(`${styles.normalSub}`);
    //Checks if drodown menu's li elements have anothere level (ul), if not the dropdown is shown as regular dropdown, not a mega menu (thanks Luka Kladaric)

    jQuery("#menu > ul").before(`<a href="#" class="${styles.menuMobile}" id="menuMobile">Navigation</a>`);

    //Adds menu-mobile class (for mobile toggle menu) before the normal menu
    //Mobile menu is hidden if width is more then 959px, but normal menu is displayed
    //Normal menu is hidden if width is below 959px, and jquery adds mobile menu
    //Done this way so it can be used with wordpress without any trouble
    
    //Make sure that menu is hidden when resizing the window to desktop
    jQuery(window).resize(function(){
      if (jQuery(window).width() > 943) {
        jQuery("#menu > ul > li").children("ul").hide();
      }
    });

    jQuery("#menu > ul > li").hover(function (e) {
      if (jQuery(window).width() > 943) {
        jQuery(this).children("ul").stop(true, false).fadeToggle(150);
        e.preventDefault();
      }
    });
    //If width is more than 943px dropdowns are displayed on hover

    jQuery("#menu > ul > li").click(function () {
      if (jQuery(window).width() <= 943) {
        jQuery(this).children("ul").fadeToggle(150);
      }
    });
    //If width is less or equal to 943px dropdowns are displayed on click (thanks Aman Jain from stackoverflow)

    //window.showOnMobileClass = `${styles.showOnMobile}`;
    jQuery("#menuMobile").click(function (e) {
      jQuery("#menu > ul").toggleClass(`${styles.showOnMobile}`);
      e.preventDefault();
    });
    return Promise.resolve<void>();
  }

In the onInit method we make use of the SP Term Store Service to load our data. The method also initializes three additional methods which we need:

_renderPlaceHolders and onDispose – Actually adds elements to the SharePoint placeholder)

private _renderPlaceHolders(): void {

    console.log('HelloWorldApplicationCustomizer._renderPlaceHolders()');
    console.log('Available placeholders: ',
      this.context.placeholderProvider.placeholderNames.map(name => PlaceholderName[name]).join(', '));
    console.log(this.context.placeholderProvider);
    // Handling the top placeholder
    if (!this._topPlaceholder) {
      this._topPlaceholder =
        this.context.placeholderProvider.tryCreateContent(
          PlaceholderName.Top,
          { onDispose: this._onDispose });

      // The extension should not assume that the expected placeholder is available.
      if (!this._topPlaceholder) {
        console.error('The expected placeholder (Top) was not found.');
        return;
      }

      if (this.properties) {
        if (this._topPlaceholder.domElement) {
          this._topPlaceholder.domElement.innerHTML = `
                <div class="${styles.app}">
                  <div class="${styles.menuContainer}">
                    <div class="${styles.menu}" id="menu"><ul></ul></div>
                  </div>
                </div>`;
        }
      }
    }
  }
  
  
  private _onDispose(): void {
          console.log('[HelloWorldApplicationCustomizer._onDispose] Disposed custom top and bottom placeholders.');
        }

generateMegaMenuLevel (Simply structures the Terms Array as HTML)

private generateMegaMenuLevel(levels: SPTermStore.ISPTermObject[]): string {
          let menuString: string = "";
      
          for (let i: number = 0; i < levels.length; i++) {
            let levelItem: SPTermStore.ISPTermObject = levels[i];
            let url: string = (typeof levelItem.localCustomProperties.url === 'undefined') ? "#" : levelItem.localCustomProperties.url;
            menuString += "<li><a href=\"" + url + "\">" + levelItem.name + "</a>";
            if (levelItem.terms.length != 0) {
              menuString += "<ul>";
              menuString += this.generateMegaMenuLevel(levelItem.terms);
              menuString += "</ul>";
            }
            menuString += "</li>";
          }
      
          return menuString;
        }

Add Data – Then Test!

Before we try out the mega menu, we should add some data. Simply navigate to the Term Store Management on the site collection, and add a new term group and a new term set (You’ll need the name).

I simply added a small demo structure like, with the Term Set name “MegaMenu”:

  • Item 1
    • Item 1.1
      • Item 1.1.1
      • Item 1.1.2
      • Item 1.1.n
    • Item 1.n
  • Level n

To add the URL where the menu item should point to, add a custom local property with the key “url”.

To test the Application Customizer we should use the command “gulp serve –nobrowser”. Your code is being compiled, and the results can be shown by navigation to the URL:

<your-tenant>.sharepoint.com/Lists/SitePages/AllItems.aspx?loadSPFX=true&debugManifestsFile=https://localhost:4321/temp/manifests.js&customActions={“<custom-action-id>“:{“location”:”ClientSideExtension.ApplicationCustomizer”,”properties”:{“TopMenuTermSet”:”<your-term-set>“}}}

Replace the <placeholders> in the URL. You can find the custom-action-id in the file MegaMenuApplicationCustomizer.manifest.json.

When navigating to the URL, you need to first click “Load Debug Scripts”, and you mega menu should be visible!

85 thoughts on “Mega Menu with SPFx Application Customizer

  1. Hey is it possible for you to upload fi I shed solution ?

    I have solution used on command bar and I am having some issues with it so wanted to use your menu instead it looks good!

  2. Hi
    Just tried out the above code. Getting some strange behavior. The megamenu drop fires on the items after it rather than on the item that should have the dropdown. Not all the time. Seems to happen after I resize the browser and go back to full screen. Have you come across this?

  3. Similar Issue to Niamh: Opening the Menu on Mobile and then resizing the screen whilst the menu is open will create an effect of a “reverse” of hover effect when in desktop. Close the Menu in Mobile before resizing the screen. This shouldn’t be an issue for most users.

  4. @Niamh & @Kenneth:
    Thanks for you feedback, I am experiencing the same issue.
    Can be fixed by adding an additional function on the resize event:

    I have modified the samples.

    jQuery(window).resize(function(){
    if (jQuery(window).width() > 943) {
    jQuery(“#menu > ul > li”).children(“ul”).hide();
    }
    });

  5. Still getting the issue. If I am anywhere near the menu when I go to a new page the dropdown fires on the wrong item on the new page. Any further help would be gratefully appreciated.

  6. An issue that I have found (and for the moment fixed) is that when there is an empty link from a termstore item (which can happen) then the href still renders as a clickable link which causes the whole page to just refresh.

    I adjusted the click event code:
    jQuery(“#menu > ul > li”).click(function () {
    if (jQuery(window).width() ul > li”).click(function (e) {
    if (jQuery(window).width() <= 1023) {
    jQuery(this).children("ul").fadeToggle(150);
    }
    //Disables Click Events for Empty Links
    if(jQuery(e.target).is("a")) {
    var addressValue = jQuery(e.target).attr("href");
    if ((addressValue.length === 0) || (addressValue.val() === '#')) {
    e.preventDefault();
    }
    }
    });

  7. How would you deploy this to SharePoint? Specifically using the Office 365 public CDN. When I try locally everything works fine, but I can’t get the menu to show up when I deploy it.

  8. I always get the “Cannot find module” error on those imports and I’m pretty shure it is the problem
    import pnp from “sp-pnp-js”;
    import * as jQuery from ‘jquery’;

    I did twice the tutorial and compared the solution I created with the one on GitHub “spfx-megamenu” but I don’t understand what is wrong.
    For the rest, I did set a simple TermSet structure as well and my URL seems to be well scructured to test it against my SP Online as I am testing other SPFx component already.

    H

    1. Hi Daniel,

      Did you type these two command line:
      npm install –save jquery@2
      npm install –save sp-pnp-js

      Thanks!

  9. Hi everyone,
    First of all thanks Jens for this code. Really appreciated.
    We are trying the make it work on the CDN of Office 365. The debut mode work, the deploy mode work but the ship mode don’t. It is like If something is not loading and it is not a missing parameter since I have hardcore the property of the term set name on the OnInt. Does someone know how to make it work on a CDN ?
    Thank you

    1. Hey guys,

      I had the same issue, debug mode works but deploy mode didn’t work (with Office 365 CDN). All good after removing the “TopMenuTermSet” parameter from the interface and hard code it inside onInit().

      export interface IMegaMenuApplicationCustomizerProperties {
      // This is an example; replace with your own property
      //TopMenuTermSet?: string;
      }

  10. I will right away grab your rss as I can’t in finding your email subscription link or e-newsletter service.
    Do you have any? Please let me recognize in order that I may just subscribe.
    Thanks.

  11. Unquestionably believe that which you stated. Your
    favorite justification seemed to be on the net
    the simplest thing to be aware of. I say to you, I definitely
    get irked while people think about worries that they plainly don’t know about.
    You managed to hit the nail upon the top as well as defined out the whole thing without having side
    effect , people could take a signal. Will likely be back to get more.
    Thanks

  12. A fascinating discussion is definitely worth comment.

    I do think that you ought to publish more about this subject matter, it
    might not be a taboo matter but typically people don’t talk about these subjects.
    To the next! Many thanks!!

  13. Hi there, i read your blog occasionally and i own a similar one and i
    was just wondering if you get a lot of spam remarks?
    If so how do you stop it, any plugin or anything you can recommend?
    I get so much lately it’s driving me mad so any help is very much appreciated.

  14. Hello There. I discovered your blog using msn. This is a really
    smartly written article. I will make sure to bookmark it
    and return to learn extra of your helpful information.
    Thank you for the post. I’ll definitely comeback.

  15. Having read this I believed it was rather enlightening.
    I appreciate you finding the time and energy to put this information together.

    I once again find myself personally spending way too much time both reading
    and posting comments. But so what, it was still worth it!

  16. Have you ever considered publishing an e-book or guest authoring on other blogs?
    I have a blog centered on the same topics you discuss and would really like to have you share some stories/information. I know my subscribers would value your work.
    If you’re even remotely interested, feel free to shoot me an e mail.

  17. I was recommended this blog by my cousin. I am
    not sure whether this post is written by him as no one else know such detailed
    about my problem. You are wonderful! Thanks!

  18. An outstanding share! I have just forwarded this
    onto a friend who has been doing a little research on this.
    And he actually ordered me dinner simply because I stumbled upon it for him…
    lol. So allow me to reword this…. Thanks for the
    meal!! But yeah, thanx for spending time to discuss this matter here
    on your blog.

  19. Hello there I am so glad I found your blog, I really found you by
    accident, while I was researching on Askjeeve for something else, Anyways
    I am here now and would just like to say kudos for a tremendous post and a all round entertaining blog
    (I also love the theme/design), I don’t have time to read through it
    all at the minute but I have saved it and also added your RSS feeds,
    so when I have time I will be back to read much more, Please do keep up the superb jo.

  20. You really make it seem so easy with your presentation but I
    find this topic to be actually something that I
    think I would never understand. It seems too complex and very broad for me.

    I am looking forward for your next post, I’ll try
    to get the hang of it!

  21. I like the valuable info you provide in your articles.
    I will bookmark your blog and check again here regularly.
    I am quite sure I will learn a lot of new stuff
    right here! Good luck for the next!

  22. Hi, I do think this is an excellent site. I stumbledupon it 😉 I may return once
    again since i have book marked it. Money and freedom
    is the best way to change, may you be rich
    and continue to guide others.

  23. Hmm it appears like your blog ate my first comment (it was extremely long) so
    I guess I’ll just sum it up what I submitted and say, I’m thoroughly enjoying your blog.
    I as well am an aspiring blog writer but I’m still new to everything.
    Do you have any tips and hints for inexperienced blog writers?
    I’d certainly appreciate it.

  24. Hey there! This is kind of off topic but I need some advice from an established blog.
    Is it very hard to set up your own blog? I’m not very techincal
    but I can figure things out pretty quick. I’m thinking
    about setting up my own but I’m not sure where to start.
    Do you have any ideas or suggestions? With thanks

  25. I loved as much as you’ll receive carried out right here.

    The sketch is attractive, your authored material stylish.
    nonetheless, you command get bought an shakiness over
    that you wish be delivering the following. unwell unquestionably come more formerly again as exactly the
    same nearly very often inside case you shield this hike.

  26. Greetings, I think your blog might be having browser
    compatibility problems. When I take a look at your blog
    in Safari, it looks fine however, when opening in Internet Explorer, it has some overlapping issues.

    I just wanted to provide you with a quick heads up!
    Other than that, excellent website!

  27. Howdy! I know this is kinda off topic however I’d figured I’d ask.
    Would you be interested in trading links or maybe guest authoring a blog article
    or vice-versa? My blog discusses a lot of the same topics as yours and I believe we could greatly
    benefit from each other. If you’re interested feel free to send me
    an email. I look forward to hearing from you! Great blog
    by the way!

  28. Thanks for a marvelous posting! I actually enjoyed reading it,
    you can be a great author. I will be sure to bookmark your blog and may come back down the road.
    I want to encourage yourself to continue your great posts,
    have a nice afternoon!

  29. Thanks , I’ve just been looking for information about this topic for a while and yours
    is the greatest I’ve discovered till now. However, what about the bottom line?
    Are you certain about the source?

  30. You could certainly see your expertise within the work you write.
    The arena hopes for even more passionate writers such as you who aren’t afraid
    to mention how they believe. At all times follow your heart.

  31. I have been browsing online more than three hours
    today, yet I never found any interesting article like yours.

    It is pretty worth enough for me. Personally, if all webmasters and bloggers made good content
    as you did, the internet will be a lot more useful than ever before.

  32. It is perfect time to make some plans for the future and it’s time to be happy.
    I’ve read this post and if I could I desire to suggest you few interesting things
    or suggestions. Perhaps you could write next
    articles referring to this article. I want to read more things about it!

  33. This design is incredible! You most certainly know how to keep a reader entertained.
    Between your wit and your videos, I was almost moved to start
    my own blog (well, almost…HaHa!) Fantastic job. I really enjoyed what you had
    to say, and more than that, how you presented it.
    Too cool!

  34. This is the part of my site where I list all of the expired domains from sites that I scrape.
    I scrape the most popular sites on the internet to produce lists of expired domains.
    All the lists are free, and I try to update them as
    often as possible.

  35. Here is where I put all of my expired web 2.0 accounts.
    I scrape massive lists and then check to see if they’re expired.

    I think you’ll be surprised by the number of accounts that I come
    across. Hopefully, you’ll be able to put these expired accounts to
    good use.

  36. Every 60 minutes there are new public proxies added.

    You can directly import these into your SEO tools or do it manually.
    There are proxies for ScrapeBox and all other tools.
    Let me know if you need free public proxies
    for other tools. I’ll try to add them if I can.

  37. distinguer juste avec le son. tel rose Certains sont chaleureux et sincères dans leurs félicitations. Pour d’autres, au contraire, on sent une certaine réserve, comme

  38. Nakliyat sektörüne 2001 tarihinde şirket yapısıyla adım atan Deha Nakliyat firmamız kısa sürede Ankara nakliyat hizmetlerinde başarıyı yakalamıştır. Sorunların yaşandığı bu sektöre çözüm bulma yönündeki çalışmalarımız zaman içerisinde görevini tamamlayarak kaliteli ve güvenli taşımacılığın oluşmasını sağlamıştır. Ankara nakliyat işlemlerinde yoğun gayret ve çaba sarf etmemizin firmamıza kazandırdığıysa müşteri memnuniyetidir.

  39. Hmm it looks like your site ate my first comment (it was extremely long) so I guess I’ll just sum it up what I wrote and say, I’m thoroughly enjoying your blog. I as well am an aspiring blog writer but I’m still new to everything. Do you have any tips and hints for first-time blog writers? I’d definitely appreciate it.

  40. Kalbimin Nehri, Sesli Sözler, Sesli Güzel Sözler, Sesli Anlamlı Sözler, Sesli Aşk Sözleri, Sesli Ünlülerin Sözleri, Ve Daha Fazlası Kalbiminnehri.net sitemizden takip edebilirsiniz

  41. Admiring the time and energy you put into your blog and in depth information you offer. It’s great to come across a blog every once in a while that isn’t the same out of date rehashed information. Fantastic read! I’ve bookmarked your site and I’m including your RSS feeds to my Google account.

  42. Appreciating the dedication you put into your website and in depth information you provide. It’s awesome to come across a blog every once in a while that isn’t the same unwanted rehashed information. Fantastic read! I’ve saved your site and I’m including your RSS feeds to my Google account.

  43. Woah! I’m really enjoying the template/theme of this website.
    It’s simple, yet effective. A lot of times it’s challenging
    to get that “perfect balance” between superb usability and visual appearance.
    I must say you have done a great job with this.
    In addition, the blog loads very quick for me on Internet explorer.
    Superb Blog!

Leave a Reply

Your email address will not be published. Required fields are marked *