01 March 2013

Resizing App Parts with PostMessage in SharePoint 2013

Hi there! 

We have moved our blog to the following address: 


http://blog.ctp.com/category/microsoft-competence/



In case you have any comment, please leave it using the new URL. Soon we will stop monitoring the blog entries in this site. Also our new material will be posted only in the new URL.

Thanks!




Introduction
In SharePoint 2013, App Parts are a great way of aggregating the content from distinct Apps on a hosted page. App Parts use iFrames to surface the App’s content and because Apps are in distinct domains than the hosted web, they cannot manipulate directly the hosted page at DOM level (for details please refer to the Same Origin Policy that web browsers enforce).
Although security constraints, there are cases where the Apps should be allowed to make changes on the hosted page. For instance, when the content the Apps are displaying does not fit into their App Parts. In this case, SharePoint 2013 allows the resizing of App Parts through the HTML5 Cross-Document Messaging. Find below an example about how this functionality works.

The Code
Find here the page of a SharePoint 2013 App which can be referenced by an App Part. The App page requests the hosted page to resize the App Part as the content grows or shrinks by using the method postMessage. The hosted page in SharePoint takes care of doing the resizing itself.
To test the code, just create a SharePoint 2013 Hosted App, add to the project a Client Web Part (known as well as App Part), create an ASPX page with the code below and set the Client Web Part to reference to that page. Afterwards, deploy the App and add the Client Web Part into a page in the hosted web. Detailed steps can be found in the References section below.


<%@ Page language="C#" Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<!-- SharePoint pages require this when displaying them in an App Part. -->
<!-- (Reference: http://msdn.microsoft.com/en-us/library/jj220046.aspx) -->
<WebPartPages:AllowFraming ID="AllowFraming" runat="server" />

<html>
<head>
    <title>App Part Communica</title>
    <script src="../Scripts/jquery-1.7.1.min.js"></script>

    <script type="text/javascript">
        // Set the style of the app part page to be consistent with the host web.
        // Get the URL of the host web and load the styling of it.
        function setStyleSheet() {
            var hostUrl = ""
            if (document.URL.indexOf("?") != -1) {
                var params = document.URL.split("?")[1].split("&");
                for (var i = 0; i < params.length; i++) {
                    p = decodeURIComponent(params[i]);
                    if (/^SPHostUrl=/i.test(p)) {
                        hostUrl = p.split("=")[1];
                        document.write("<link rel=\"stylesheet\" href=\"" + hostUrl +
                            "/_layouts/15/defaultcss.ashx\" />");
                        break;
                    }
                }
            }
            // if no host web URL was available, load the default styling
            if (hostUrl == "") {
                document.write("<link rel=\"stylesheet\" " +
                    "href=\"/_layouts/15/1033/styles/themable/corev15.css\" />");
            }
        }
        setStyleSheet();
    </script>
</head>

<body style="background-color: #f5f5f5">

    <!-- the content considered for the resizing -->
    <div id="content">
        <p>SenderId: <span id="senderId"></span></p>
        <input type="button" onclick="Communica.Part.addItem();" value="Add Item"/>&nbsp;
        <input type="button" onclick="Communica.Part.removeItem();" value="Remove Item"/>
        <ul id="itemsList">
            <li>Item</li>
        </ul>
    </div>

    <script lang="javascript">
        "use strict";

        // define a namespace
        window.Communica = window.Communica || {};

        $(document).ready(function () {
            // initialise
            Communica.Part.init();
        });

        Communica.Part = {
            senderId: '',      // the App Part provides a Sender Id in the URL parameters,
                               // every time the App Part is loaded, a new Id is generated.
                               // The Sender Id identifies the rendered App Part.
            previousHeight: 0, // the height
            minHeight: 0,      // the minimal allowed height
            firstResize: true, // On the first call of the resize the App Part might be
                               // already too small for the content, so force to resize.

            init: function () {
                // parse the URL parameters and get the Sender Id
                var params = document.URL.split("?")[1].split("&");
                for (var i = 0; i < params.length; i = i + 1) {
                    var param = params[i].split("=");
                    if (param[0].toLowerCase() == "senderid")
                        this.senderId = decodeURIComponent(param[1]);
                }

                // find the height of the app part, uses it as the minimal allowed height
                this.previousHeight = this.minHeight = $('body').height();

                // display the Sender Id
                $('#senderId').text(this.senderId);

                // make an initial resize (good if the content is already bigger than the
                // App Part)
                this.adjustSize();
            },

            adjustSize: function () {
                // Post the request to resize the App Part, but just if has to make a resize

                var step = 30, // the recommended increment step is of 30px. Source:
                               // http://msdn.microsoft.com/en-us/library/jj220046.aspx
                    width = $('body').width(),        // the App Part width
                    height = $('body').height() + 7,  // the App Part height
                                                      // (now it's 7px more than the body)
                    newHeight,                        // the new App Part height
                    contentHeight = $('#content').height(),
                    resizeMessage =
                        '<message senderId={Sender_ID}>resize({Width}, {Height})</message>';

                // if the content height is smaller than the App Part's height,
                // shrink the app part, but just until the minimal allowed height
                if (contentHeight < height - step && contentHeight >= this.minHeight) {
                    height = contentHeight;
                }

                // if the content is bigger or smaller then the App Part
                // (or is the first resize)
                if (this.previousHeight !== height || this.firstResize === true) {
                    // perform the resizing

                    // define the new height within the given increment
                    newHeight = Math.floor(height / step) * step +
                        step * Math.ceil((height / step) - Math.floor(height / step));

                    // set the parameters
                    resizeMessage = resizeMessage.replace("{Sender_ID}", this.senderId);
                    resizeMessage = resizeMessage.replace("{Height}", newHeight);
                    resizeMessage = resizeMessage.replace("{Width}", width);
                                        // we are not changing the width here, but we could

                    // post the message
                    window.parent.postMessage(resizeMessage, "*");

                    // memorize the height
                    this.previousHeight = newHeight;

                    // further resizes are not the first ones
                    this.firstResize = false;
                }
            },

            addItem: function () {
                // add an item to the list
                $('#itemsList').append('<li>Item</li>');
                Communica.Part.adjustSize();
            },

            removeItem: function () {
                // remove an item from the list
                $('#itemsList li:last').remove();
                Communica.Part.adjustSize();
            }
        };
    </script>
</body>
</html>

Conclusion
An App page can resize its App Part by posting messages to the hosted page. The end result is a more responsive UI as follows:
The App's page (grey background) in an App Part.

After the content grew, the App Part has resized.
If the content shrinks, the code requests the App Part to be reduced.



References