SharePoint, AngularJS & Material Design

What this does...

This page layout will make a REST call to any list or library anywhere within SharePoint (cross-origin problems notwithstanding).

All that needs to be done is to set 2 page properties. The url of the site where the list/library is housed and the Display Name of that list. Once that is done the items on the list will be displayed in a fancy tile manner and will load more results as the screen is scrolled to the bottom.

Here's what it will look like:
pretty tiles Nice!

Setup

First, to follow this exactly, you will need to be on a publishing site. You can adapt this pretty easily if you prefer to go the external app route.

Folder Structure

+-- .../_catalogs/masterpage                     // Root Folder
|   +-- custom                                   // Custom Master / Layouts Folder
|   |   +-- custom-masterpage                    // Custom Master Page Folder
|   |   |   +-- custom-master.html               // Custom Master Page HTML
|   |   +-- list-display                         // List Display Layout Folder
|   |   |   +-- list-display-layout.html         // List Display Layout HTML
|   |   |   |   +-- app                          // App folder for list display
|   |   |   |   |   +-- app.js                   // Main App - Angular Module
|   |   |   |   |   +-- require-config.js        // Configure Require
|   |   |   |   |   +-- css                      // Styles for this App
|   |   |   |   |   |   +-- list-display.css     // CSS styles specific to this view
|   |   |   |   |   +-- components               // Angular Components
|   |   |   |   |   |   +-- editview             // Component that shows in edit mode
|   |   |   |   |   |   |   +-- editview.html    // HTML Partial
|   |   |   |   |   |   |   +-- editview.js      // Edit - Angular Module
|   |   |   |   |   |   |   +-- editview.spec.js // Edit - Jasmine Test
|   |   |   |   |   |   +-- listview             // Component that shows in edit mode
|   |   |   |   |   |   |   +-- listview.html    // HTML Partial
|   |   |   |   |   |   |   +-- listview.js      // Display - Angular Module
|   |   |   |   |   |   |   +-- listview.spec.js // Display - Jasmine Test
|   |   +-- Scripts                              // Scripts Folder for shared scripts
|   |   |   +-- lib                              // Folder for js libraries
|   |   |   |   +-- angular                      // Angular Library
|   |   |   |   +-- jasmine                      // Jasmine library
|   |   |   |   +-- require                      // RequireJS

Inside the angular folder I have another folder for each of the following:

  • angular
  • angular-animate
  • angular-aria
  • angular-loader
  • angular-material
  • angular-messages
  • angular-mocks
  • angular-new-router

You will need to get these files, the easiest way is to use nodejs (which I describe below), but you could choose to do a git clone or copy/paste from the repositories. You only need the "dist" folder from the angular new router and you do not need to upload the Demo folder from Angular Material Design to your SharePoint Masterpage directory.

Getting the Libraries

Make sure you have Node.js installed on your machine. If you don't, head over to Node.js and follow the instructions there. The Bower module will require Visual Studio to compile, the free 2010 version will work perfectly (you can use other versions, just let npm know which version it is with this command npm config set msvs_version 2013 --global substituting your version).

Create a directory where you want the files to download. Inside that directory a directory named app and create 2 files, package.json and bower.json.

Here are the contents:

package.json

{
  "name": "my-app",
  "private": true,
  "version": "0.1.0",
  "description": "AngularJS, RequireJS, Material Design, Angular New Router."
  "dependencies": {
    "bower": "^1.3.1",
    "jasmine-core": "^2.3.0"
  },
  "scripts": {
    "preinstall": "npm prune",
    "postinstall": "bower install"
  }
}

bower.json

{
  "name": "my-app",
  "description": "A starter project for AngularJS",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "angular": "1.4.x",
    "angular-new-router": "~0.5.x",
    "angular-loader": "1.4.x",
    "angular-mocks": "~1.4.x",
    "angular-material": "~0.10.x",
    "requirejs-text": "2.0.x",
    "requirejs": "2.1.x"
  }
}

Now open an Administrator Command Window in this folder and execute npm install. You can then just copy all of the folders from the bower directory (inside the app folder) and paste them into the libraries in your SharePoint Masterpage gallery. Jasmine will actually be in the node-modules directory in the root application folder. You can thin these out a little and only select the javascript files (I choose the js, js.min and js.min.map generally), but be careful to not over-prune.

If you have any troubles, just find these items on git and clone or copy and paste.

Page Layout Content Type

Make a new Content Type for your site (or preferably on your hub so it is everywhere). Give it a fancy name, like List Display Page Layout. The Parent Content Type is HTML Page Layout and can be found in the Publishing Content Types group. Use a folder of your choice (I created one just for custom page layouts).

Add 2 new columns. Site URL and List Name (both as single line of text).

The Fun Part

HTML Layout

Create a new HTML Layout Page in the 'list-display' folder, name it 'list-display-layout' and associate the new 'List Display Page Layout' content type you made (you can't associate the content type in SP Designer, so you need to edit the properties on the web - there are other options that make use of PowerShell or a C# console app, but that is beyond the scope of this article).

Open the Page Layout in SP Designer (or other preferred editor). There is a short descriptive paragraph at the top of the page code, this contains the link to the snippet generator. I just want to make sure you can find this later.

Look for this tag <!--MS:<asp:ContentPlaceHolder id="PlaceHolderAdditionalPageHead" runat="server">--> it's usually around line 20 give or take a line or 2.

You are going to link the stylesheets and the RequireJS configuration here. place these right before this tag <!--MS:<Publishing:EditModePanel runat="server" id="editmodestyles">-->. When linking CSS files, be sure to set the ms-design-conversion to no.

Here are the tags you will add:

<link href="../Scripts/lib/angular/angular-material/angular-material.min.css" rel="stylesheet" type="text/css" ms-design-css-conversion="no" />  
<link href="app/css/list-display.css" rel="stylesheet" type="text/css" ms-design-css-conversion="no" />  
<script type="text/javascript" data-main="/_catalogs/masterpage/custom/list-display/app/require-config" src="../Scripts/lib/require/require.min.js"></script>  

Now find the main content area, look for this tag <!--MS:<asp:ContentPlaceHolder ID="PlaceHolderMain" runat="server">-->. It should have a throwaway div that shows where your content will be, delete this.

We are going to make a view for both display and edit modes, but will use a single angular app with differing views.

Make a container for our list, set it up with the MainController and establish the Material Design Layout.

<div id="list-container" ng-controller="MainController as main" layout="row" flex="100" layout-wrap="">  
  <!-- SHOW IN EDIT MODE -->
  <!-- END EDIT MODE -->
  <!-- SHOW IN DISPLAY MODE -->
  <!-- END DISPLAY MODE -->
</div>  

Remember the link to the snippet gallery. Head there now. In the drop down for Edit Mode Panel, select show in edit mode only. Copy the snippet and paste it between the Edit Mode comment tags. Repeat this, but select show in regular mode.

On both of these add these attributes to the first div layout="row" flex="100" layout-align="center center"

Find this tag <!--MS:<Publishing:EditModePanel runat="server">-->. For the Edit Mode add these attributes CssClass="edit-mode-panel" layout="row" flex="85" right after the runat attribute. For the Display Mode the attributes to add are layout="row" flex="100".

It should look something like this

<div id="list-container" ng-controller="MainController as main" layout="row" flex="100" layout-wrap="">  
  <!-- SHOW IN EDIT MODE -->            
    <div data-name="EditModePanelShowInEdit" layout="row" flex="100" layout-align="center center">
      <!--CS: Start Edit Mode Panel Snippet-->
        <!--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" CssClass="edit-mode-panel" layout="row" flex="85">-->
          <!--PS: Start of READ-ONLY PREVIEW (do not modify)--><!--PE: End of READ-ONLY PREVIEW-->
            <div class="DefaultContentBlock" style="border:medium black solid; background:yellow; color:black; margin:20px; padding:10px;">
              You should replace this div with content that renders based on your Edit Mode Panel Properties.
            </div>            
          <!--PS: Start of READ-ONLY PREVIEW (do not modify)--><!--PE: End of READ-ONLY PREVIEW-->
        <!--ME:</Publishing:EditModePanel>-->
      <!--CE: End Edit Mode Panel Snippet-->
    </div>
  <!-- END EDIT MODE -->
  <!-- SHOW IN DISPLAY MODE -->
    <div data-name="EditModePanelShowInRead" layout="row" flex="100">
      <!--CS: Start Edit Mode Panel Snippet-->
        <!--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" PageDisplayMode="Display" layout="row" flex="100">-->
          <!--PS: Start of READ-ONLY PREVIEW (do not modify)--><!--PE: End of READ-ONLY PREVIEW-->
            <div class="DefaultContentBlock" style="border:medium black solid; background:yellow; color:black; margin:20px; padding:10px;">
              You should replace this div with content that renders based on your Edit Mode Panel Properties.
            </div>
          <!--PS: Start of READ-ONLY PREVIEW (do not modify)--><!--PE: End of READ-ONLY PREVIEW-->
        <!--ME:</Publishing:EditModePanel>-->
      <!--CE: End Edit Mode Panel Snippet-->
    </div>            
  <!-- END DISPLAY -->
</div>  

Both of these items have default content blocks, you need to replace these with something worthwhile. Let's start with the Display Mode.

<div id="display-container" layout="column" flex="100">  
  <div ng-viewport="displaylistview" layout="row" layout-align="center center" flex="100"></div>
</div>  

You should probably head over to the Angular Material site to get an understanding of what the layout and flex tags are about, basically it's a grid.

The ng-viewport tag is how the New Angular Router identifies which partial to fill this area with. Future versions of the router will use ng-outlet, so if it is slightly in the future and you are using Angular 1.5, you need to change that.

Cool, next we'll put in some Edit Mode stuff.

<div id="edit-container" layout="column" flex="100">  
  <div layout="row" flex="100">
    <div layout="column" flex="" ng-form="listSelector">  
      <md-content  class="md-padding" layout="row" layout-sm="column">
        <md-input-container layout-padding="true" layout="column" flex="50">
          <!--PAGE FIELD: SITE URL GOES HERE-->
        </md-input-container>
        <md-input-container layout-padding="true" layout="column" flex="50">
          <!--PAGE FIELD: LIST NAME GOES HERE-->
        </md-input-container>
      </md-content>
      <div ng-viewport="editlistview" flex=""></div>
    </div>
  </div>
</div>  

Back to the Snippet Gallery. Generate a snippet for the Site Url page field (this comes from the content type you created earlier). Paste this where shown, same deal with the List Name.

These should look like this.

<div data-name="Page Field: Site URL"><!--CS: Start Page Field: Site URL Snippet--><!--SPM:<%@Register Tagprefix="PageFieldTextField" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>--><!--MS:<PageFieldTextField:TextField FieldName="c50f5d23-50f5-4cb1-82a7-b59b91d8b60e" runat="server">--><!--PS: Start of READ-ONLY PREVIEW (do not modify)--><div align="left" class="ms-formfieldcontainer"><div class="ms-formfieldlabelcontainer" nowrap="nowrap"><span class="ms-formfieldlabel" nowrap="nowrap">Site URL</span></div><div class="ms-formfieldvaluecontainer">Site URL field value.</div></div><!--PE: End of READ-ONLY PREVIEW--><!--ME:</PageFieldTextField:TextField>--><!--CE: End Page Field: Site URL Snippet-->  
</div>  

The ng-viewport here won't do much for a while. In a later tutorial, you will use this spot to select columns to filter the display... It's pretty neat.

Before we move to the next piece, here's the whole 'PlaceHolderMain'

<!--MS:<asp:ContentPlaceHolder ID="PlaceHolderMain" runat="server">-->  
      <div id="list-container" ng-controller="MainController as main" layout="row" flex="100" layout-wrap="">
            <!-- SHOW IN EDIT MODE -->            
            <div data-name="EditModePanelShowInEdit" layout="row" flex="100" layout-align="center center">
                  <!--CS: Start Edit Mode Panel Snippet-->
                  <!--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" CssClass="edit-mode-panel" layout="row" flex="85">-->
                          <!--PS: Start of READ-ONLY PREVIEW (do not modify)--><!--PE: End of READ-ONLY PREVIEW-->
                            <div id="edit-container" layout="column" flex="100">
                              <div layout="row" flex="100">
                                    <div layout="column" flex="" ng-form="listSelector">  
                                      <md-content  class="md-padding" layout="row" layout-sm="column">
                                            <md-input-container layout-padding="true" layout="column" flex="50">
                                              <div data-name="Page Field: Site URL"><!--CS: Start Page Field: Site URL Snippet--><!--SPM:<%@Register Tagprefix="PageFieldTextField" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>--><!--MS:<PageFieldTextField:TextField FieldName="c50f5d23-50f5-4cb1-82a7-b59b91d8b60e" runat="server">--><!--PS: Start of READ-ONLY PREVIEW (do not modify)--><div align="left" class="ms-formfieldcontainer"><div class="ms-formfieldlabelcontainer" nowrap="nowrap"><span class="ms-formfieldlabel" nowrap="nowrap">Site URL</span></div><div class="ms-formfieldvaluecontainer">Site URL field value.</div></div><!--PE: End of READ-ONLY PREVIEW--><!--ME:</PageFieldTextField:TextField>--><!--CE: End Page Field: Site URL Snippet-->
                                              </div>
                                            </md-input-container>
                                            <md-input-container layout-padding="true" layout="column" flex="50">
                                              <div data-name="Page Field: List Name"><!--CS: Start Page Field: List Name Snippet--><!--SPM:<%@Register Tagprefix="PageFieldTextField" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>--><!--MS:<PageFieldTextField:TextField runat="server" FieldName="c0286c92-1e05-446c-9e77-ea65f6fb4980">--><!--PS: Start of READ-ONLY PREVIEW (do not modify)--><div align="left" class="ms-formfieldcontainer"><div class="ms-formfieldlabelcontainer" nowrap="nowrap"><span class="ms-formfieldlabel" nowrap="nowrap">List Name</span></div><div class="ms-formfieldvaluecontainer">List Name field value.</div></div><!--PE: End of READ-ONLY PREVIEW--><!--ME:</PageFieldTextField:TextField>--><!--CE: End Page Field: List Name Snippet-->
                                              </div>
                                            </md-input-container>
                                      </md-content>
                                      <div ng-viewport="editlistview" flex=""></div>
                                    </div>
                              </div>
                            </div>
                            <!--PS: Start of READ-ONLY PREVIEW (do not modify)--><!--PE: End of READ-ONLY PREVIEW-->
                      <!--ME:</Publishing:EditModePanel>-->
                    <!--CE: End Edit Mode Panel Snippet-->
              </div>
              <!-- END EDIT MODE -->        
              <!-- SHOW IN DISPLAY MODE -->     
              <div data-name="EditModePanelShowInRead" layout="row" flex="100">
                    <!--CS: Start Edit Mode Panel Snippet-->
                    <!--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" PageDisplayMode="Display" layout="row" flex="100">-->
                            <!--PS: Start of READ-ONLY PREVIEW (do not modify)--><!--PE: End of READ-ONLY PREVIEW-->
                            <div id="display-container"  layout="column" flex="100">
                                  <div ng-viewport="displaylistview" layout="row" layout-align="center center" flex="100"></div>
                            </div>
                            <!--PS: Start of READ-ONLY PREVIEW (do not modify)--><!--PE: End of READ-ONLY PREVIEW-->
                      <!--ME:</Publishing:EditModePanel>-->
                    <!--CE: End Edit Mode Panel Snippet-->
              </div>            
              <!-- END DISPLAY -->
      </div>
<!--ME:</asp:ContentPlaceHolder>-->  

RequireJS

I like to use require with my front end trickery. SharePoint is already so resource heavy with it's own scripts, I want to be sure I only load what I really need. I'm not going to get too in depth about require here, I've done that before, so here's the require-config file.

'use strict';

require.config({  
    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',

        angular: 'Scripts/lib/angular/angular/angular.min',
        angularAria: 'Scripts/lib/angular/angular-aria/angular-aria.min',
        angularAnimate: 'Scripts/lib/angular/angular-animate/angular-animate.min',
        angularRoute: 'Scripts/lib/angular/angular-new-router/dist/router.es5.min',
        angularMaterial: 'Scripts/lib/angular/angular-material/angular-material.min',
        angularMessages: 'Scripts/lib/angular/angular-messages/angular-messages.min',
        angularMocks: 'Scripts/lib/angular/angular-mocks/angular-mocks.min',
        angularMaterialMocks: 'Scripts/lib/angular/angular-material/angular-material-mocks.min',
        text: 'Scripts/lib/require/text',
        app: 'list-display/app/app'
    },
    shim: {
        'angular' : {'exports' : 'angular'},
        'angularRoute': ['angular'],
        'angularAria': ['angular'],
        'angularAnimate': ['angular'],
        'angularMaterial': ['angular'],
        'angularMessages': ['angular'],
        'angularMocks': {
            deps:['angular'],
            'exports':'angular.mock'
        },
        'angularMaterialMocks': {
            deps:['angular'],
            'exports':'angularMaterial.mock'
        },
        'app': 'app'
    },
    priority: [
        "angular"
    ],
});

require([  
    'angular',
    'app'
    ], function(angular, app) {
        var $listContainer = angular.element(document.getElementById('list-container'));
        angular.element(document).ready(function() {
            angular.bootstrap($listContainer, ['listApp'], { strictDi: true });
        });
    }
);

Basically, this tells require where all the libraries are and once the document is ready it Bootstraps the Angular app to the list container (starting the app).

Main App

The main app here contains the routes that determine which partial to load, as well as a dataservice factory that can be used throughout the application to make REST calls. This is the app.js file.

First we are going to make sure we have the required components loaded, and return the 'listApp' module we bootstrapped in the RequireJS file.

define([  
  'angular',
  'angularAnimate',
  'angularMaterial',
  'angularRoute',
  'list-display/app/components/editview/editview',
  'list-display/app/components/listview/listview'
],
function(angular, animate, material, angularRoute){  
  // Declare app level module which depends on views, and components
  return angular.module('listApp', [])
  .controller()
  .factory()
  .config()
  .run()
  // functions go here
});

The listApp module has dependencies, a controller, a factory, a config block and a run block. So build those out (I am just going to put the dependencies in here and assume that you know what is going on - if you don't head over to the Angular docs).

return angular.module('listApp', [  
  'ngNewRouter',
  'ngMaterial',
  'listApp.editview',
  'listApp.listview'
])  
.controller('MainController', ['$location', '$router', MainController])
.factory('dataservice', ['$http', '$log', dataservice]) // this will get page / list info in config   
.config(['$componentLoaderProvider', config])
.run(['$location', '$rootScope', '$routeParams', run]);
// functions come immediately after run

You can see that the edit and list views have their own modules that will be used.

Now for all of those functions.

function config($componentLoaderProvider){  
  $componentLoaderProvider.setTemplateMapping(function(name) {
    return _spPageContextInfo.siteAbsoluteUrl 
      + '/_catalogs/masterpage/custom/list-display/app/components/'+name+'/'+name+'.html';
  });
}

This makes the new angular router look in the right directory for the components.

function run($location, $rootScope, $route){  
  var original = $location.path;    
  $location.path = function(path, reload) {
    if(reload === false) {
      var lastRoute = $route.current,
        un = $rootScope.$on('$locationChangeSuccess', function() {
          $route.current = lastRoute;
        });
    }
    return original.apply($location, [path]);
  };
};

A nifty little piece that allows the url to be changed without reloading the whole page.

function MainController($location, $router){  
  var vm = this;
  if(!window.PageState)
    vm.isEdit = 0
  else
    vm.isEdit = PageState.ViewModeIsEdit || 0

  if(vm.isEdit == 1)
    $location.path('/edit-view', false)
  else
    $location.path('/display-view', false)
  $router.config([
    {
      path: '/display-view',
      components: { displaylistview: 'listview' }
    },
    {
      path: '/edit-view',
      components: { editlistview: 'editview' }
    }
  ]);
}

The main controller has one job here. It determines if SharePoint is in Edit Mode or Display Mode, then it appends the url appropriately and tells the app which component to load.

function dataservice($http, $log) {  
  return {
    sendRequest: sendRequest
  };

  function sendRequest($http, reqParams) {
    var request = {
      method: reqParams.method,
      url: reqParams.url,
      headers: reqParams.headers,
      params: reqParams.params || {},
      data: reqParams.data || '',
      cache: true
    };

    return $http(request).then(handleSuccess, handleError);

    function handleSuccess(response) {
      var results = typeof response.data != 'object' ? JSON.parse(response).data : response.data;
      return results;
    }

    function handleError(err) {
      $log.error('XHR failed for sendRequest: ' + err.data);
    }
  }
}

This service can be used throughout the app, by passing in the parameters, you will get the data to show on the page.

Edit View Module

The Edit View does everything it has to at the moment, but in the next post I am going to show how to make this more robust. You just have to make the module so Angular doesn't throw an error.

editview.js

'use strict';

define([  
  'angular',
  'angularAria',
  'angularAnimate',
  'angularMaterial'
], 
function(angular, aria, animate, material) {  
  angular.module('listApp.editview', ['ngMaterial'])    
    .controller('EditviewController', [EditviewController]);

  function EditviewController(){ return false; }
});

editview.html

<div>Hello Edit World!</div>  

Display View Module

The Display View has a lot more going on.

Start by defining the module and its services:

'use strict';

define([  
  'angular',
  'angularAria',
  'angularAnimate',
  'angularMaterial'
], 
function(angular, aria, animate, material, messages, angularRoute) {  
  angular.module('listApp.listview', ['ngMaterial'])    
    .controller('ListviewController', ['dataservice','$http', '$log', '$window', '$rootScope', ListviewController])
    .directive('onFinishRender',['$timeout', '$parse', onFinishRender]);
// FUNCTIONS GO HERE
});

Cool, now the functions

function onFinishRender($timeout, $parse) {  
  return {
    restrict: 'A',
    link: function (scope, element, attr) {
      if (scope.$last === true) {
        $timeout(function () {
          scope.$emit('ngRepeatFinished');
          if(!!attr.onFinishRender){
            $parse(attr.onFinishRender)(scope);
          }
        });
      }
    }
  }
}

This attaches to the ng-repeat in the partial and emits an event when the last item is rendered. This is necessary for the IE hack to keep everything in view.

function ListviewController(dataservice, $http, $log, $window, $rootScope) {  
  var 
    vm = this, 
    spWorkSpace = angular.element(document.getElementById('s4-workspace')), 
    spBody = angular.element(document.getElementById('s4-bodyContainer')),
    tileLength = 0,
    reqCount = 0;
  vm.pageProperties = {},
  vm.results = [],
  vm.reqParams = {};

  // after getting the items bind scroll event to load more
  activate().then(function(){
    spWorkSpace.bind('scroll', itemLoader);
  });    

  function activate() {          
    return getPageProps().then(function() {
      $log.log('Got the page props!');
      return getListItems().then(function() {
        $log.log('Got the items!');
      });
    });
  }

  // IE hack the css for material design pushes items off the screen in IE, by setting the container height this is prevented.
  $rootScope.$on('ngRepeatFinished', function(ngRepeatFinishedEvent) {
    var
      spBody = document.getElementById('s4-bodyContainer'),
      mdContainer = document.getElementById('display-container'),
      mdGrid = document.querySelectorAll('#display-container > div > md-content > md-grid-list')[0],
      mdContent = document.querySelectorAll('#display-container > div > md-content')[0];
    spBody.style.height = mdContainer.style.height = mdGrid.offsetHeight+'px';
    mdContent.style.top = '0px';
  });

  // Loads more items once the last tile has scrolled onto the screen
  function itemLoader(event) {
    var
      tiles = document.querySelectorAll('#display-container > div > md-content > md-grid-list > md-grid-tile'),
      tileCount = tiles.length,
      lastTile = tiles[tileCount - 1],
      lastTileOffsetTop = lastTile.offsetTop,
      currentLocation = spWorkSpace[0].clientHeight + spWorkSpace[0].scrollTop,        
      reqUrl = vm.reqParams.listParams.nextRequest.url,

      workSpaceHeight = spWorkSpace[0].clientHeight;

    // If there is a next URL, and the there are more tiles than there used to be
    // and the last tile has scrolled onto the screen get more items
    if( reqUrl !== false && tileLength < tileCount && lastTileOffsetTop < currentLocation  ) { 
      tileLength = tileCount;
      vm.reqParams.listParams.currentRequest = vm.reqParams.listParams.nextRequest;
      getListItems(vm.reqParams.listParams.nextRequest).then(function() {
      });
    }
  }

  function getPageProps() {
    var
      baseUrl    = _spPageContextInfo.webAbsoluteUrl,
      listId     = _spPageContextInfo.pageListId.substring(1, _spPageContextInfo.pageListId.length-1),
      itemId     = _spPageContextInfo.pageItemId,
      pageParams = {
        method: 'GET',
        url: baseUrl + "/_api/web/lists(guid'"+listId+"')/items("+itemId+")",
        headers: {
            'accept': 'application/json; odata=verbose',
            'content-type': 'application/json; odata=verbose'
        } 
      };

      // Get the selected site and list from the page properties
      return dataservice.sendRequest($http, pageParams).then(function(data) {        
        vm.pageProperties = {
          id: data.d.Id,
          siteUrl: data.d.Site_x0020_URL,
          listName: data.d.List_x0020_Name
        };
        vm.reqParams.listParams = { 
          method: 'GET',
          url: vm.pageProperties.siteUrl + "/_api/web/lists/getbytitle('"+ vm.pageProperties.listName +"')/items",
          params: {
            $top: '22',
            $select: ['*', 'Author/EMail', 'Author/Title', 'Editor/EMail', 'Editor/Title', 
              'ParentList/BaseTemplate', 'ParentList/BaseType', 'File/MajorVersion', 'File/MinorVersion',
              'File/Name', 'File/ServerRelativeUrl'
            ].join(','),
            $expand:'Author,Editor,ParentList,File'
          },
          headers: {
            'accept': 'application/json; odata=verbose',
            'content-type': 'application/json; odata=verbose'
          }
        };  
        vm.reqParams.listParams.initialRequest = vm.reqParams.listParams || {};
        vm.reqParams.listParams.currentRequest = vm.reqParams.listParams.initialRequest;
        return vm;
      });
  }

  function getListItems(params){
    var reqParams = params || vm.reqParams.listParams.initialRequest; 

    return getItems($http, reqParams);

    function getItems ($http, params) {
      return dataservice.sendRequest($http, params).then(function(data) {
        var 
          results = data.d.results,
          resultsWithProps = buildGridModel({
            background: ''
          });
        vm.reqParams.listParams.nextRequest = vm.reqParams.listParams.nextRequest || vm.reqParams.listParams.currentRequest || {};
        vm.reqParams.listParams.nextRequest.url = data.d.__next || false;
        vm.reqParams.listParams.nextRequest.params = {};
        vm.reqParams.listParams.prevRequest = vm.reqParams.listParams.currentRequest;

        function buildGridModel(tileTmpl) {
          var it;
          for(var i = 0, l = results.length; i < l; i++) {
            var 
              j = calculateJ(j, i, 10),
              result = results[i],
              wUrl = $window.location;

            it = angular.extend({}, tileTmpl);
            it.span = { row: 1, col: 1 };
            it.summary = result.Description || result.Comments || 'No Summary Supplied.';
            it.footers = {
              author: result.Author.Title,
              authorEmail: result.Author.EMail,
              createdDate: result.Created,
              editor: result.Editor.Title,
              editorEmail: result.Editor.EMail,
              modifiedDate: result.Modified
            };

            // Document Library
            if(result.ParentList.BaseType == 1){
              it.title = result.File.Name;
              it.url = wUrl.origin + result.File.ServerRelativeUrl + '?Web=1';
              it.version = result.File.MajorVersion +'.'+ result.File.MinorVersion;
            }
            // Link List
            else if(result.ParentList.BaseTemplate == 103) {
              it.title = result.URL.Description;
              it.url = result.URL.Url;
            }
            // List
            else {
              it.title = result.Title;
              it.url = vm.pageProperties.siteUrl + '/Lists/' + vm.pageProperties.listName + 'DispForm.aspx?ID=' + result.ID;
            }

            // switch-case to change colors and sizes of tiles
            switch(j+1) {
              case 1:
                it.background = "red";
                it.span.row = it.span.col = 2;
                break;
              case 2: it.background = "green";         break;
              case 3: it.background = "darkBlue";      break;
              case 4:
                it.background = "blue";
                it.span.col = 2;
                break;
              case 5:
                it.background = "yellow";
                it.span.row = it.span.col = 2;
                break;
              case 6: it.background = "pink";          break;
              case 7: it.background = "darkBlue";      break;
              case 8: it.background = "purple";        break;
              case 9: it.background = "deepBlue";      break;
              case 10: it.background = "lightPurple";  break;
              case 11: it.background = "yellow";       break;
            }
            vm.results.push(it);
          }
          return vm.results;

          // function to determine if more than 11 items to reset switch-case
          function calculateJ(jVal, index, max) {
            var jValue = jVal;
            if(typeof jValue == 'undefined' || index < max)
              return index
            else if (jValue != 0 && jValue % max == 0)
              return 0
            else
              return ++jValue
          }              
        }   
      });
    }
  }
}

Here's the Display View Partial
listview.html

<md-content layout-padding flex="85">  
  <md-grid-list
      md-cols-sm="1" md-cols-md="2" md-cols-gt-md="6"
      md-row-height-gt-md="1:1" md-row-height="4:3"
      md-gutter="8px" md-gutter-gt-sm="4px">
    <md-grid-tile ng-repeat="tile in listview.results"
                  md-rowspan="{{tile.span.row}}"
                  md-colspan="{{tile.span.col}}"
                  md-colspan-sm="1"
                  ng-class="tile.background"
                  on-finish-render >
      <md-grid-tile-header>
        <a ng-href="{{tile.url}}"><h2>{{ tile.title }}</h2></a>
      </md-grid-tile-header>
      <div>{{ tile.summary }}</div>
      <md-grid-tile-footer>
        <div layout="row">
          <h6 flex>Author: {{tile.footers.author}}</h6>
          <h6 flex>Created: {{tile.footers.createdDate | date:'MM/dd/yyyy'}}</h6>
        </div>
        <div layout="row">
          <h6 flex>Editor: {{tile.footers.editor}}</h6>
          <h6 flex>Modified: {{tile.footers.modifiedDate | date:'MM/dd/yyyy'}}</h6>
        </div>
      </md-grid-tile-footer>
    </md-grid-tile>
  </md-grid-list>
</md-content>  

Style

Now for a few styles.
list-display.css

.gray {
  background: #f5f5f5; }
.green {
  background: #b9f6ca; }
.yellow {
  background: #ffff8d; }
.blue {
  background: #84ffff; }
.darkBlue {
  background: #80d8ff; }
.deepBlue {
  background: #448aff; }
.purple {
  background: #b388ff; }
.lightPurple {
  background: #8c9eff; }
.red {
  background: #ff8a80; }
.pink {
  background: #ff80ab; }
md-grid-tile {  
  transition: all 300ms ease-out 50ms; }
md-grid-tile md-icon {  
  padding-bottom: 32px; }
md-grid-tile md-grid-tile-footer {  
  background: rgba(0, 0, 0, 0.68);
  height: 36px; }
md-grid-tile-footer figcaption {  
  width: 100%; }
md-grid-tile-footer figcaption h3 {  
  margin: 0;
  font-weight: 700;
  width: 100%;
  text-align: center; }
input {  
  height: 2em;
}
input.ms-long{  
    width: 98%;
}

Stay tuned. I am going to update this to better describe the Display View Controller and in the next part of this series, we are going to do some cool things with the Edit View.