SharePoint MasterPage & Layouts with RequireJS

A Funny Thing Happened...

I was happily coding away. I made a nifty new layout page, it used RequireJS to load up my angular files and bootstrap the app. (See my previous post SharePoint with Angular & Require). It was pretty, it did what I wanted it to, except...

It bypassed the RequireJS configuration from the MasterPage. Ugh. Anyway, I should have known you can't configure Require 2x. What to do?

Well here's what I came up with. In the MasterPage HTML file I add the script tag for require.min.js just before the SharePoint:CustomJSUrl tag, but I don't put the data-main attribute in there. Like this:

<script type="text/javascript" src="../Scripts/lib/require/require.min.js"></script>  
<!--SPM:<SharePoint:CustomJSUrl runat="server"/>-->  

That will get require ready to go.

Then I changed the require configuration file for the MasterPage app to be a module that returns a configuration object, but that does not execute the config.

config-require.js

define(function(){  
  var rConfig = {
    baseUrl: '/_catalogs/masterpage/custom/',
    deps: [
      'spruntime_js','sp_js'
    ],
    paths: {
      // SharePoint Runtime & Core
      // Register any SharePoint Library if it is required to be processed first
      spruntime_js: window.location.origin + '/_layouts/15/sp.runtime',
      sp_js: window.location.origin + '/_layouts/15/sp',

      // Frameworks
      angular: 
        ['https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min',
         'Scripts/lib/angular/angular/angular'],
      angularAria: 
        ['https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular-aria.min',
         'Scripts/lib/angular/angular-aria/angular-aria'],
      angularAnimate: 
        ['https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular-animate.min',
         'Scripts/lib/angular/angular-animate/angular-animate'],
      angularMaterial: 
        ['https://ajax.googleapis.com/ajax/libs/angular_material/0.11.0/angular-material.min',
         'Scripts/lib/angular/angular-material/angular-material'],
      angularRoute: 'Scripts/lib/angular/angular-new-router/dist/router.es5.min',
      text: 'Scripts/lib/require/text',

      // Standalone Scripts
      twitter: 'Scripts/lib/twitter',
      css: 'Scripts/lib/css-loader',

      // Modules
      boot: 'custom-masterpage/app/start',
      navFlyout: 'Modules/nav-flyout/app/modules/nav-flyout'
    },
    shim: {
      'twitter': 'twitter',
      'css': 'css',
      'angular' : {'exports' : 'angular'},
      'angularRoute': ['angular'],
      'angularAria': ['angular'],
      'angularAnimate': ['angular'],
      'angularMaterial': ['angular'],
      'app': 'app'
    },
    priority: [
      'angular'
    ]
  };

  return {
    config : rConfig
  }
});

Then I make use of the additional page head place holders to inject information into the configuration object before configuring Require.

Here's the MasterPage set up.

<!--MS:<SharePoint:AjaxDelta id="DeltaPlaceHolderAdditionalPageHead" Container="false" runat="server">-->  
  <!--MS:<asp:ContentPlaceHolder id="PlaceHolderAdditionalPageHead" runat="server">-->
  <!--ME:</asp:ContentPlaceHolder>-->
  <!--MS:<SharePoint:DelegateControl runat="server" ControlId="AdditionalPageHead" AllowMultipleControls="true">-->
  <!--ME:</SharePoint:DelegateControl>-->
  <!--MS:<asp:ContentPlaceHolder id="PlaceHolderBodyAreaClass" runat="server">-->
  <!--ME:</asp:ContentPlaceHolder>-->
  <script type="text/javascript">
    requirejs(['/_catalogs/masterpage/custom/custom-masterpage/app/config-require.js'], function(common) {
      requirejs.config(common.config);
      requirejs(['css'], function() {
        loadCss('/_catalogs/masterpage/custom/Modules/nav-flyout/app/css/nav-flyout.css');
        requirejs(['boot'], function(boot) {
          boot.init();
          requirejs(['layoutInit']);
        });
      });
    }); 
  </script>
<!--ME:</SharePoint:AjaxDelta>-->  

The important thing about the Script here is that it is placed right before the closing tag for DeltaPlaceHolderAdditionalPageHead and that it is after all the other tags that are in here too.

This is because this tag

<!--MS:<asp:ContentPlaceHolder id="PlaceHolderAdditionalPageHead" runat="server">-->  
<!--ME:</asp:ContentPlaceHolder>-->  

will place the additional page head tags from the layout page before the configuration when the page is rendered.

You may also have noticed that the configuration object did not contain a 'layoutInit' path for this statement requirejs(['layoutInit']);. That's OK, our layout page will inject that into the configuration object.

I would also suggest that you create a default layoutInit module that is empty so that you can use layout pages that do not change the configuration object.

So now we can look at the Layout page and see what's happening there.

<!--MS:<asp:ContentPlaceHolder id="PlaceHolderAdditionalPageHead" runat="server">-->  
  <!--CS: Start Edit Mode Panel Snippet-->
    <!--SPM:<%@Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>-->
    <!--SPM:<%@Register Tagprefix="Publishing" Namespace="Microsoft.SharePoint.Publishing.WebControls" Assembly="Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>-->        
    <!--MS:<Publishing:EditModePanel runat="server" id="editmodestyles">-->
      <!--MS:<SharePoint:CssRegistration name="&#60;% $SPUrl:~sitecollection/Style Library/~language/Themable/Core Styles/editmode15.css %&#62;" After="&#60;% $SPUrl:~sitecollection/Style Library/~language/Themable/Core Styles/pagelayouts15.css %&#62;" runat="server">-->
      <!--ME:</SharePoint:CssRegistration>-->
    <!--ME:</Publishing:EditModePanel>-->
  <!--CE: End Edit Mode Panel Snippet-->          
  <script type="text/javascript">
    requirejs(['/_catalogs/masterpage/custom/custom-masterpage/app/config-require.js'], function(common) {
      common.config.paths.layoutInit = 'list-display/app/appInit';
    });
  </script>
<!--ME:</asp:ContentPlaceHolder>-->  

So, just before the closing tag for the additional page head placeholder, I add a path to the layouts appInit module - which just bootstraps the angular application.

Here's how the tags are rendered on the page:

<script type="text/javascript" src="/_catalogs/masterpage/custom/Scripts/lib/require/require.min.js"></script>  
<script type="text/javascript">  
  requirejs(['/_catalogs/masterpage/dbs-custom/custom-masterpage/app/config-require.js'], function(common) {
    common.config.paths.layoutInit = 'list-display/app/appInit';
  });
</script>  
<script type="text/javascript">  
  requirejs(['/_catalogs/masterpage/custom/custom-masterpage/app/config-require.js'], function(common) {
    requirejs.config(common.config);
    requirejs(['css'], function() {
      loadCss('/_catalogs/masterpage/custom/Modules/nav-flyout/app/css/nav-flyout.css');
      requirejs(['boot'], function(boot) {
        boot.init();
        requirejs(['layoutInit']);
      });
    });
  }); 
</script>  

That wasn't so bad.

Until next time.