best practice for integration of jquery plugin into durandal knockout SPA

390 views Asked by At

I am a newb to durandal and knockout. I recently started a project working with the VS2013 hot towel template. Turns out the code is somewhat out of date and I have worked through some of the initial getting started issues. I am attempting to integrate a nav menu related jquery plugin. However, I having trouble hooking up the plugin functionality with the nav menu found in my shell.html view.

here is my main.js file:

requirejs.config({
  paths: {
    'text': '../scripts/vendor/require/text',
    'durandal': '../scripts/vendor/durandal/js',
    'plugins': '../scripts/vendor/durandal/js/plugins',
    'transitions': '../scripts/vendor/durandal/js/transitions',
    'knockout': '../scripts/vendor/knockout/knockout-3.1.0',
    'bootstrap': '../scripts/vendor/bootstrap/bootstrap',
    'toastr': '../scripts/vendor/toastr/toastr',
    'jquery': '../scripts/vendor/jquery/jquery-2.1.4',
    'logger': 'services/logger',
    'theme' : '../scripts/js/idealTheme',
    'global' : '../scripts/js/functions'
  },
  shim: {
    'bootstrap': {
      deps: ['jquery'],
      exports: 'jQuery'
    },
    'toastr': {
      deps: ['jquery'],
      exports: 'jQuery'
    },
    'theme': {
      deps: ['jquery'],
      exports: 'jQuery'
    }
    ,
    'global': {
      deps: ['jquery'],
      exports: 'jQuery'
    }
  }
});

define(['durandal/system', 'durandal/app', 'durandal/viewLocator', 'plugins/router', 'logger'], function (system, app, viewLocator, router, logger) {
  //>>excludeStart("build", true);
  system.debug(true);
  //>>excludeEnd("build");

  app.title = 'TestApp';

  app.configurePlugins({
    router: true,
    dialog: true
  });

  app.start().then(function () {
    //Replace 'viewmodels' in the moduleId with 'views' to locate the view.
    //Look for partial views in a 'views' folder in the root.
    viewLocator.useConvention();

    //Show the app by setting the root view model for our application with a transition.
    app.setRoot('viewmodels/shell', 'entrance');

    // override bad route behavior to write to 
    // console log and show error toast
    router.handleInvalidRoute = function (route, params) {
      logger.logError('No route found', route, 'main', true);
    };
  });
});

and my shell.js

define(['durandal/system',
  'plugins/router',
  'durandal/app',
  'toastr',
  'theme',
  'logger'
  ],
  function (system, router, app, toastr, theme, logger) {
    var vm = this;       
    var router = router;       
    var search = function () {
      toastr.info('Search is not yet implemented...');
      //app.showMessage('Search not yet implemented...');
    };

    var activate =  function activate() {
       $("#nav_menu").idealtheme();
        logger.log('Loaded!', null, system.getModuleId(vm), true);
      }
    vm = {
      router: router,
      showSubMenu: showSubMenu,
      search: search,
      activate: activate
    }

    return vm;
  });

relevant piece of my shell.html

<div id="main_wrapper">
  <header id="site_header">

        <!-- End Top Search -->
        <nav id="main_nav">
          <div id="nav_menu">
            <span class="mobile_menu_trigger">
              <a href="#" class="nav_trigger"><span></span></a>
            </span>
            <ul id="navy" class="clearfix" >
              <li class="normal_menu mobile_menu_toggle current_page_item">
                <a href="index.html"><span>Home</span></a>
                <ul>
                  <li class="normal_menu"><a href="index.html">Home Page V1</a></li>
                  <li class="normal_menu"><a href="index2.html">Home Page V2</a></li>
                  <li class="normal_menu"><a href="index3.html">Home Page V3</a></li>
                  <li class="normal_menu"><a href="index4.html">Home Page V4</a></li>
                  <li class="normal_menu"><a href="index5.html">Home Page V5</a></li>
                  <li class="normal_menu">
                    <a href="index-one-page1.html">Home One Page </a>
                    <ul>
                      <li class="normal_menu"><a href="index-one-page1.html">Home One Page V1</a></li>
                      <li class="normal_menu"><a href="index-one-page2.html">Home One Page V2</a></li>
                    </ul>
                  </li>

finally, the theme plugin file

(function ($) {

  //========> Menu
  $.fn.idealtheme = function (options) {
    var whatTheLastWidth = getScreenWidth();
    var ifisdescktop = false;
    var MqL = 1170;

    var settings = {
      duration: 300,
      delayOpen: 0,
      menuType: "horizontal", // horizontal - vertical 
      position: "right", // right - left
      parentArrow: true,
      hideClickOut: true,
      submenuTrigger: "hover",
      backText: "Back to ",
      clickToltipText: "Click",
    };
    $.extend(settings, options);
    var nav_con = $(this);
    var $nav_con_parent = nav_con.parent("#main_nav");
    var menu = $(this).find('#navy');

    //=====> Mega Menu Top Space
    function megaMenuTop() {
      $(menu).find('.has_mega_menu').each(function () {
        var top_space = $(this).parent('li').outerHeight();
        $(this).find(' > .mega_menu').css({ "top": top_space + "px", "width": "100%" });
      });
    }
    megaMenuTop();

    //=====> Vertical and Horizontal    
    if (settings.menuType == "vertical") {
      $(menu).addClass("vertical_menu");
      if (settings.position == "right") {
        $(menu).addClass("position_right");
      } else {
        $(menu).addClass("position_left");
      }
    } else {
      $(menu).addClass("horizontal_menu");
    }

    //=====> Add Arrows To Parent li
    if (settings.parentArrow === true) {
      $(menu).find("li.normal_menu li, li.has_image_menu").each(function () {
        if ($(this).children("ul").length > 0) {
          $(this).children("a").append("<span class='parent_arrow normal_menu_arrow'></span>");
        }
      });

      $(menu).find("ul.mega_menu li ul li, .tab_menu_list > li").each(function () {
        if ($(this).children("ul").length > 0) {
          $(this).children("a").append("<span class='parent_arrow mega_arrow'></span>");
        }
      });
    }

    function TopSearchFunc() {
      $(".top_search").each(function (index, element) {
        var top_search = $(this);
        top_search.submit(function (event) {
          event.stopPropagation();
          if (top_search.hasClass("small_top_search")) {
            top_search.removeClass("small_top_search");
            top_search.addClass("large_top_search");
            if (getScreenWidth() <= 315) {
              top_search.siblings("#top_cart").animate({ opacity: 0 });
            }
            top_search.siblings("#nav_menu:not(.mobile_menu), .logo_container").animate({ opacity: 0 });
            return false;
          }

        });
        $(top_search).on("click touchstart", function (e) {
          e.stopPropagation();
        });
        $(document).on("click touchstart", function (e) {
          if (top_search.hasClass("large_top_search")) {
            top_search.removeClass("large_top_search");
            top_search.addClass("small_top_search");
            if (getScreenWidth() <= 315) {
              top_search.siblings("#top_cart").animate({ opacity: 1 });
            }
            top_search.siblings("#nav_menu:not(.mobile_menu), .logo_container").animate({ opacity: 1 });
          }
        });
      });
      if (getScreenWidth() < 1190) {
        $("#navigation_bar").find(".top_search").addClass("small_top_search");
      } else {
        $("#navigation_bar").find(".top_search").removeClass("small_top_search");
      }
    }
    var top_search_func = new TopSearchFunc();

    $(window).resize(function () {
      top_search_func = new TopSearchFunc();
      megaMenuTop();
      if (whatTheLastWidth > 992 && getScreenWidth() <= 992 && $("body").hasClass("header_on_side")) {
        $(menu).slideUp();
      }
      if (whatTheLastWidth <= 992 && getScreenWidth() > 992 && $("body").hasClass("header_on_side")) {
        $(menu).slideDown();
      }

      if (whatTheLastWidth <= 992 && getScreenWidth() > 992 && !$("body").hasClass("header_on_side")) {
        resizeTabsMenu();
        removeTrigger();
        playMenuEvents();
      }
      if (whatTheLastWidth > 992 && getScreenWidth() <= 992) {
        releaseTrigger();
        playMobileEvents();
        resizeTabsMenu();
        $(menu).slideUp();
      }
      whatTheLastWidth = getScreenWidth();
      return false;
    });

    //======> After Refresh
    function ActionAfterRefresh() {
      if (getScreenWidth() <= 992 || $("body").hasClass("header_on_side")) {
        releaseTrigger();
        playMobileEvents();
        resizeTabsMenu();

      } else {
        resizeTabsMenu();
        removeTrigger();
        playMenuEvents();
      }
    }

    var action_after_ref = new ActionAfterRefresh();

    //======> Mobile Menu
    function playMobileEvents() {
      $(".nav_trigger").removeClass("nav-is-visible");
      $(menu).find("li, a").unbind();
      if ($(nav_con).hasClass("mobile_menu")) {
        $(nav_con).find("li.normal_menu").each(function () {
          if ($(this).children("ul").length > 0) {
            $(this).children("a").not(':has(.parent_arrow)').append("<span class='parent_arrow normal_menu_arrow'></span>");
          }
        });
      }
      megaMenuEvents();

      $(menu).find("li:not(.has-children):not(.go-back)").each(function () {
        $(this).removeClass("opened_menu");
        if ($(this).children("ul").length > 0) {
          var $li_li_li = $(this);
          $(this).children("a").on("click", function (event) {
            var curr_act = $(this);

            if (!$(this).parent().hasClass("opened_menu")) {
              $(this).parent().addClass("opened_menu");
              $(this).parent().siblings("li").removeClass("opened_menu");
              if ($(this).parent().hasClass("tab_menu_item")) {
                $(this).parent().addClass("active");
                $(this).parent().siblings("li").removeClass("active");
              }
              $(this).siblings("ul").slideDown(settings.duration);
              $(this).parent("li").siblings("li").children("ul").slideUp(settings.duration);
              setTimeout(function () {
                var curr_position = curr_act.offset().top;
                $('body,html').animate({
                  //scrollTop: curr_position ,
                }, { queue: false, duration: 900, easing: "easeInOutExpo" }
                );
              }, settings.duration);

              return false;
            }
            else {
              $(this).parent().removeClass("opened_menu");
              $(this).siblings("ul").slideUp(settings.duration);
              if ($li_li_li.hasClass("mobile_menu_toggle") || $li_li_li.hasClass("tab_menu_item")) {
                return false;
              }
            }
          });
        }
      });
    }

    function megaMenuEvents() {
      $(menu).find('li.has_mega_menu ul').removeClass("moves-out");
      $(menu).find('.go-back, .mega_toltip').remove();
      $(menu).find('li.has_mega_menu > ul').hover(function () {

        $(this).find(".mega_menu_in ul").each(function (index, element) {
          var $mega_ul = $(this);
          var its_height = 0;

          $mega_ul.children('li').each(function (index, element) {
            var ul_li_num = $(this).innerHeight();
            its_height += ul_li_num;
          });
          $mega_ul.attr("data-height", its_height);
        });
      });
      $(menu).find('ul.mega_menu li li').each(function (index, element) {
        var $mega_element = $(this);
        if ($mega_element.children('ul').length > 0) {
          $mega_element.addClass("has-children");
          $mega_element.children('ul').addClass("is-hidden");
        }
      });
      $(menu).find('ul.mega_menu li.has-children').children('ul').each(function (index, element) {
        var $mega_ul = $(this);
        var its_height = 0;
        $mega_ul.children('li').each(function (index, element) {
          var ul_li_num = $(this).innerHeight();
          its_height += ul_li_num;
        });
        $mega_ul.attr("data-height", its_height);

        var $mega_link = $mega_ul.parent('li').children('a');
        var $mega_title = $mega_ul.parent('li').children('a').text();
        $("<span class='mega_toltip'>" + settings.clickToltipText + "</span>").prependTo($mega_link);

        if (!$mega_link.find('.go-back').length) {
          $("<li class='go-back'><a href='#'>" + settings.backText + $mega_title + "</a></li>").prependTo($mega_ul);
        }

      });

      $(menu).find('ul.mega_menu li.has-children').children('a').on('click', function (event) {
        event.preventDefault();
        var selected = $(this);

        if (selected.next('ul').hasClass('is-hidden')) {
          var ul_height = parseInt(selected.next('ul').attr("data-height"));
          var link_height = parseInt(selected.innerHeight());
          var all_height = ul_height + link_height;

          selected.addClass('selected').next('ul').removeClass('is-hidden').end().parent('.has-children').parent('ul').addClass('moves-out');
          selected.closest('.mega_menu_in').animate({ height: all_height });

          selected.parent('.has-children').siblings('.has-children').children('ul').addClass('is-hidden').end().children('a').removeClass('selected');
          //====> if is mobile
          if (selected.closest('#nav_menu').hasClass("mobile_menu")) {
            selected.parent('.has-children').removeClass("mega_parent_hidden").prevAll('li').slideUp(settings.duration);
          }

        }

      });

      //submenu items - go back link
      $('.go-back').on('click', function () {
        var link_height = parseInt($(this).parent("ul").parent("li").parent("ul").attr("data-height"));

        $(this).parent('ul').addClass('is-hidden').parent('.has-children').parent('ul').removeClass('moves-out');
        $(this).closest('.mega_menu_in').animate({ height: link_height });
        //====> if is mobile
        if ($(this).closest('#nav_menu').hasClass("mobile_menu")) {
          $(this).parent('ul').parent('li').removeClass("mega_parent_hidden").prevAll('li').slideDown(settings.duration);
        }

        return false;
      });
    }


    //======> Desktop Menu
    function playMenuEvents() {
      $(menu).children('li').children('ul').hide(0);
      $(menu).find("li, a").unbind();
      $(menu).slideDown(settings.duration);
      $(menu).find('ul.tab_menu_list').each(function (index, element) {
        var tab_link = $(this).children('li').children('a');
        $("<span class='mega_toltip'>" + settings.clickToltipText + "</span>").prependTo(tab_link);
        $(this).children('li').on('mouseover', function () {
          if (!$(this).hasClass('active')) {
            $(this).children('ul').stop().fadeIn();
            $(this).siblings().children('ul').stop().fadeOut();
            $(this).addClass('active');
            $(this).siblings().removeClass('active');
          }
        });
      });

      megaMenuEvents();

      $(menu).find('li.normal_menu, > li').hover(function () {
        var li_link = $(this).children('a');
        $(this).children('ul').stop().fadeIn(settings.duration);
      }, function () {
        $(this).children('ul').stop().fadeOut(settings.duration);
      });
    }

    //======> Trigger Button Mobile Menu
    function releaseTrigger() {
      $(nav_con).find(".nav_trigger").unbind();
      $(nav_con).addClass('mobile_menu');
      $nav_con_parent.addClass('has_mobile_menu');

      $(nav_con).find('.nav_trigger').each(function (index, element) {
        var $trigger_mob = $(this);
        $trigger_mob.on('click touchstart', function (e) {
          e.preventDefault();
          if ($(this).hasClass('nav-is-visible')) {
            $(this).removeClass('nav-is-visible');
            $(menu).slideUp(settings.duration);

          } else {
            $(this).addClass('nav-is-visible');
            $(document).unbind("click");
            $(document).unbind("touchstart");
            $(menu).slideDown(settings.duration, function () {
              $(menu).on("click touchstart", function (event) {
                event.stopPropagation();
              });
              $(document).on('click touchstart', function (event) {
                if ($trigger_mob.hasClass('nav-is-visible') && getScreenWidth() <= 992) {
                  $trigger_mob.removeClass('nav-is-visible');
                  $(menu).slideUp(settings.duration);
                }
              });

            });
          }
        });

      });

    }

    //=====> get tabs menu height
    function resizeTabsMenu() {
      function thisHeight() {
        return $(this).outerHeight();
      }
      $.fn.sandbox = function (fn) {
        var element = $(this).clone(), result;
        element.css({ visibility: 'hidden', display: 'block' }).insertAfter(this);
        element.attr('style', element.attr('style').replace('block', 'block !important'));
        var thisULMax = Math.max.apply(Math, $(element).find("ul:not(.image_menu)").map(thisHeight));
        result = fn.apply(element);
        element.remove();
        return thisULMax;
      };
      $(".tab_menu").each(function () {
        $(this).css({ "height": "inherit" });
        if (!$(nav_con).hasClass("mobile_menu")) {
          var height = $(this).sandbox(function () { return this.height(); });
          $(this).height(height);
        }

      });
    }
    resizeTabsMenu();
    //=====> End get tabs menu height

    function removeTrigger() {
      $(nav_con).removeClass('mobile_menu');
      $nav_con_parent.removeClass('has_mobile_menu');
    }

    //----------> sticky menu
    enar_sticky();

    function getScreenWidth() {
      return document.documentElement.clientWidth || document.body.clientWidth || window.innerWidth;
    }

    //----------> sticky menu   
    function enar_sticky() {
      if ($.isFunction($.fn.sticky)) {
        var $navigation_bar = $("#navigation_bar");
        $navigation_bar.unstick();
        var mobile_menu_len = $navigation_bar.find(".mobile_menu").length;
        var side_header = $(".header_on_side").length;
        if (mobile_menu_len === 0 && side_header === 0) {
          $navigation_bar.sticky({
            topSpacing: 0,
            className: "sticky_menu",
            getWidthFrom: "body"
          });
        } else {
          $navigation_bar.unstick();
        }
      }
    }

  };
})( jQuery );

I am basically trying to get the plugin functionality to work with durandal and knockout. I have attempted to hook the theme function to a DOM element in my view model with no success. I have also attempted to simply place the lines of code needed to display the sub menus within my view model also with no success.

I tried creating a function to handle the mouse over event of list item elements and display the sub menus with no success. I get an error message that the "children" function is not defined. I would really like to get the entire plugin to work but will take baby steps until that is accomplished.

 var showSubMenu = function (data, event) {
      $parent = event.currentTarget;
      $parent.children('ul').stop().fadeIn();
    };

and in my shell.html attached an event binding like this:

 <li data-bind="event: { mouseover: showSubMenu }" class="normal_menu mobile_menu_toggle current_page_item">

Any information or help one can provide is appreciated.

thanks,

1

There are 1 answers

3
Anish Patel On

Option 1 You could wrap the plugin in a define like this:

define('idealtheme', ['jquery'], function($){
   // idealtheme script goes here

   return $;// return the jquery object, you don't really have to return anything
}

And then in my shell I'd "require" the plugin, which will detect that jquery is required and download that first and then run your plugin script before the durandal composition happens:

define(['durandal/system', 'durandal/app', 'durandal/viewLocator', 'plugins/router', 'logger', 'idealtheme'], function (system, app, viewLocator, router, logger, $) {
   // your startup logic

Option 2 Or you could just include jquery and other dependencies that are packaged as requirejs modules in your html file without requirejs, such as:

<script type="text/javascript" src="~/scripts/vendor/jquery-2.1.4.js"></script>
<script type="text/javascript" src="~/scripts/vendor/knockout-3.1.0.js"></script>    
<script type="text/javascript" src="~/scripts/vendor/bootstrap.js"></script>
<script type="text/javascript" src="~/scripts/js/idealtheme.js"></script>
<script type="text/javascript" src="~/scripts/vendor/toastr.js"></script>
<script type="text/javascript" src="~/scripts/js/functions.js"></script>
<script type="text/javascript" src="~/scripts/vendor/require.js" data-main="main"></script>

In your main.js file, change the requirejs config remove the paths you've now included as script tags and remove the shim setting to look like this:

requirejs.config({
  paths: {
    'text': '../scripts/vendor/require/text',
    'durandal': '../scripts/vendor/durandal/js',
    'plugins': '../scripts/vendor/durandal/js/plugins',
    'transitions': '../scripts/vendor/durandal/js/transitions',
    'logger': 'services/logger'
  }
});

Then, while still in your main.js file, define the modules that would return something from the ones you've just removed from "paths" in the requirejs config like this:

define('jquery', function(){ return jQuery;});
define('toastr', function(){ return toastr;});

Finally, continue as you were:

define(['durandal/system', 'durandal/app', 'durandal/viewLocator', 'plugins/router', 'logger'], function (system, app, viewLocator, router, logger) {
  //>>excludeStart("build", true);
  system.debug(true);
  //>>excludeEnd("build");

  app.title = 'TestApp';

  app.configurePlugins({
    router: true,
    dialog: true
  });

  app.start().then(function () {
    //Replace 'viewmodels' in the moduleId with 'views' to locate the view.
    //Look for partial views in a 'views' folder in the root.
    viewLocator.useConvention();

    //Show the app by setting the root view model for our application with a transition.
    app.setRoot('viewmodels/shell', 'entrance');

    // override bad route behavior to write to 
    // console log and show error toast
    router.handleInvalidRoute = function (route, params) {
      logger.logError('No route found', route, 'main', true);
    };
  });
});