CHUVASH.eu

CHunky Universe of Vigourous Astonishing SHarepoint :)

Tag Archives: javascript

Client Side Rendering with Async dependencies

Yesterday I asked a question on SharePoint StackExchange:

I also asked Elio Struyf on Twitter:

Good idea, Elio Struyf! Now I want to try it out.

Preparations

In this case I’ll be using my example from my blog post yesterday:

Drag and Drop Image using Client Side Rendering

I have created a new list and added a lookup field to my previous list. What I get is a Title of the lookup item, but not my custom field called DragAndDrop. In my test I will try to load the DragAndDrop Image using an ajax call and rendering it after Client Side Rendering is done with my item.

To be complete, I want to show some screenshots for my lookup field:

csr-async-001csr-async-002

It will result in this OOTB rendering:
csr-async-003

Trying out CSR with async dependencies

While working with jslink, first of all I want to show a loading image instead of an empty html element, to show that something is loading:
csr-async-004

Here is the skeletton of the jslink file:

(function () {
    'use strict';

    var display = function (ctx, field) {
        var containerId = "tolle";
        var loadingImg = _spPageContextInfo.webAbsoluteUrl + "/_layouts/images/loadingcirclests16.gif";
        //unfortunately SP.Utilities.Utility is not defined at this stage
        //SP.Utilities.Utility.getImageUrl("loadingcirclests16.gif");       
        return ['<div id="', containerId, '"><img src="', loadingImg, '"/></div>'].join('');
    };

    var overrideContext = {};
    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates.Fields = {
        'TolleLookup': {
            'DisplayForm': display
        }
    };

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext);
})();

Making an ajax call

Next step is to initiate an ajax call. I am trying to avoid the jQuery dependency. There is so much you can do with the built-in javascript functions. For making an ajax call I am using SP.RequestExecutor.js

Here is the result:

csr-async-005

The code should be quite self explaining:

(function () {
    'use strict';

    var onDataRetrieved = function(response) {
        console.log("yippie");
        var data = JSON.parse(response.body);
        var imgSrc = data.d.DragAndDropImage;
        var container = document.getElementById("tolle");
        container.innerHTML = ['<img src="', imgSrc, '"/>'].join('');
    }

    var onError = function(response) {
        console.error("failed", response);
    }
    var initiateAjaxCall = function(ctx) {
        var item = ctx.CurrentItem;
        var fieldName = ctx.CurrentFieldSchema.Name;
        var fieldValue = item[fieldName];
        
        var itemId = fieldValue.split(";#")[0];
        var lookupListId = ctx.ListSchema.Field[0].LookupListId;
        var url = [window._spPageContextInfo.webAbsoluteUrl,
            "/_api/web/lists/getbyid('",
            lookupListId,
            "')/Items(",
            itemId, ")?$select=DragAndDropImage"].join("");
        SP.SOD.registerSod('sp.requestexecutor.js', '/_layouts/15/sp.requestexecutor.js');
        SP.SOD.executeFunc("sp.requestexecutor.js", "SP.RequestExecutor", function () {
            var executor = new SP.RequestExecutor(window._spPageContextInfo.webAbsoluteUrl);
            executor.executeAsync(
                {
                    url: url,
                    method: "GET",
                    headers: { "Accept": "application/json; odata=verbose" },
                    success: onDataRetrieved,
                    error: onError
                }
            );
        });
    }
    var display = function (ctx, field) {
        if (!ctx.CurrentItem[ctx.CurrentFieldSchema.Name]) { //if there is no value
            return "";
        }
        var containerId = "tolle";
        var loadingImg = window._spPageContextInfo.webAbsoluteUrl + "/_layouts/images/loadingcirclests16.gif";
        initiateAjaxCall(ctx);
        //unfortunately SP.Utilities.Utility is not defined at this stage
        //SP.Utilities.Utility.getImageUrl("loadingcirclests16.gif");       
        return ['<div id="', containerId, '"><img src="', loadingImg, '"/></div>'].join('');
    };

    var overrideContext = {};
    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates.Fields = {
        'TolleLookup': {
            'DisplayForm': display
        }
    };

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext);
})();

Next steps and further considerations

It seems to work, although I have some considerations:

  1. What happens if the data from the ajax call is retrieved before Client Side Rendering is done (then the container is not rendered yet). Even if the risk for that is low, it should be handled properly.
  2. It would be good to have a consistent look and feel in the Display form and in the list view. To make it possible, following should be done:
    1. We must create references to html elements (containers) with unique ids, need to implement logic for generating ids and keeping track of the right elements.
    2. Ajax calls should be bundled, otherwise it will hugely impact the performance, even in a list view with 30 items.
Advertisements

Drag and Drop Image using Client Side Rendering

I continue my series about Client Side Rendering (CSR) and jsgrid. Today I want to try a custom field where users can drag and drop images. The inspiration comes from:

What I want to achieve is:

  1. A custom field that is rendered with jslink
  2. Users can drag and drop small pictures (thumbnails) into the field
  3. A base64 image representation is saved as the field value
  4. Optionally implement pasting images using Clipboard API

Step 1 Create a field with a custom jslink

Create a field of type Note. I am using the PnP Core Extensions to make it quickier:

dnd-001

My jslink file is very simple to begin with:

(function () {
    'use strict';
    function view(ctx, field) {
        return "hello";
    }

    var overrideContext = {};

    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates.Fields = {
        'DragAndDropImage': {
            'View': view,
            'DisplayForm': view
            //'EditForm': verySimpleNewAndEdit,
            //'NewForm': verySimpleNewAndEdit
        }
    };

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext);
})();

This will result in the following display form. Just outputting “hello” indicates that my field is jslink are registered correctly:

dnd-002

Step 2. Ensure base64 works in the field

Not all fields will work. I have tried many of them and found that the field type Note with Plain text works for saving base64 images.

First download some sample icons at flaticons.com

dnd-003

Convert an icon to a base64 image using dataurl.net:

dnd-004

Save the string into the Drag And Drop Image field:

dnd-005

Now update the view template in the CSR overrides:

//http://stackoverflow.com/a/822486/632117
//in display form we have to strip the html elements that are pasted in by SharePoint
function strip(html) {
    var tmp = document.createElement("DIV");
    tmp.innerHTML = html;
    return tmp.textContent || tmp.innerText || '';
}
function renderImage(fieldValue) {
    if (fieldValue) {
        return ['<img style="max-width:100px;" src="',
            strip(fieldValue), '"/>'].join('');
    }
    return '';
}

function view(ctx, field) {
    //disable editing in Quick Edit mode
    if (ctx.inGridMode) {
        field.AllowGridEditing = false;
    }
    var fieldValue = ctx.CurrentItem[ctx.CurrentFieldSchema.Name];
    return renderImage(fieldValue);
}

Well, it works:

dnd-006dnd-007

 Step 3. Implement drag and drop

Let’s start with the edit template:

function renderDropZone(fieldValue) {
    var img = renderImage(fieldValue);
    return ['<div id="drop_zone">', img, '</div>'].join('');
}

function edit(ctx, field) {
    var formCtx = SPClientTemplates.Utility.GetFormContextForCurrentField(ctx);
    formCtx.registerGetValueCallback(formCtx.fieldName, function () {
        //will implement the logic later
        return '';
    });
    var fieldValue = ctx.CurrentItem[ctx.CurrentFieldSchema.Name];
    return renderDropZone(fieldValue);
}

var overrideContext = {};
overrideContext.Templates = overrideContext.Templates || {};
overrideContext.Templates.Fields = {
    'DragAndDropImage': {
        'View': view,
        'DisplayForm': view,
        'EditForm': edit,
        'NewForm': edit
    }
};

I have wrapped the image element with a div (id=drop_zone). Now I need some css to show it:

var additionalStyle = [
    '<style>',
    '#drop_zone { height: 100px; width:100px; background: #efe}',
    '</style>'
].join('');
document.write(additionalStyle);

Now there is a clear “drop zone”:
dnd-008
To make it as simple as possible, let’s follow oroboto’s drag and drop code. Here we go:

var processFiles = function (event) {
    var element = event.target;
    event.stopPropagation();
    event.preventDefault();
    removeDropZoneClass(event);

    // FileList object of File objects
    var files = event.dataTransfer.files;
    for (var i = 0, f; f = files[i]; i++) {
        var reader = new FileReader();

        // closure to capture file info
        reader.onload = (function (file, index) {
            return function (e) {
                var dataUri = e.target.result;
                var img = renderImage(dataUri);
                element.innerHTML = img;
            };
        })(f, i);

        // read file as data URI
        reader.readAsDataURL(f);
    }

};

var highlightDropZone = function (e) {
    e.stopPropagation();
    e.preventDefault();
    document.getElementById('drop_zone').classList.add('highlight');
}

var removeDropZoneClass = function (e) {
    document.getElementById('drop_zone').classList.remove('highlight');
}

function prepareDropZone(ctx) {
    if (ctx.inGridMode) {
        return;
    }
    // add event listeners if File API is supported
    var dropZone = document.getElementById('drop_zone');
    if (window.File &amp;amp;&amp;amp; window.FileReader &amp;amp;&amp;amp; window.FileList &amp;amp;&amp;amp; window.Blob) {
        dropZone.addEventListener('drop', processFiles, false);
        dropZone.addEventListener('dragover', highlightDropZone, false);
        dropZone.addEventListener('dragenter', highlightDropZone, false);
        dropZone.addEventListener('dragleave', removeDropZoneClass, false);
    } else {
        dropZone.innerHTML = 'The File APIs are not fully supported in this browser.';
        dropZone.className = 'highlight';
    }

};

// ... code omitted for brevity

overrideContext.Templates.OnPostRender = prepareDropZone;

That’s not bad, not bad:

dnd-009

The last thing in this step is to save the base64 value

formCtx.registerGetValueCallback(formCtx.fieldName, function () {
    var dropZone = document.getElementById('drop_zone');
    var img = dropZone.getElementsByTagName('img');
    return img.length ? img[0].src : '';
});

Step 4 Support pasting images

Drag And Drop is cool. But what about pasting images. Wouldn’t it be nice? The functionality is called Clipboard API. You can check which browsers support this on caniuse.com. The simplest example I’ve found is on stackoverflow: How does onpaste work

The code I use is as follows:

document.onpaste = function (event) {
    var items = (event.clipboardData || event.originalEvent.clipboardData).items;
    console.log(JSON.stringify(items)); // will give you the mime types
    var blob = items[0].getAsFile();
    var reader = new FileReader();
    reader.onload = function (e) {
        var dataUri = e.target.result;
        var img = renderImage(dataUri);
        dropZone.innerHTML = img;
    }; // data url!
    reader.readAsDataURL(blob);
}

Here is how pasting works:
dnd-011

Step 5 Error handling (Not done yet)

I need to ensure that

  • only images are allowed
  • only small images are allowed, otherwise base64 will be to heavy
  • proper handling of old browsers, we don’t need to support them, but the users should get good information about what they need to make it work
  • a help icon is shown, when clicked a help callout is shown about how it works

Step 6 Support Quick Edit (Not done yet)

We need code that works in Quick Edit:

  • need to write code for jsgrid
  • need handling of multiple drop zones on the same page (unique ids and smart handling of pasting)

Summary

This post is about discovering what’s possible to do with client side rendering and using today’s technologies like html5 and new javascript apis. I hope you’ve got some inspiration how normal sharepoint fields combined with Client Side Rendering (CSR) api can create more value in your project, in Office 365 and in SharePoint On Premises. Please leave feedback in comments.

Fulll source code

And finally, here comes the most up-to-date full source code for this custom Drag And Drop Image field:

(function () {
    'use strict';
    var additionalStyle = [
        '<style>',
        '#drop_zone { height: 100px; width:100px; background: #efe}',
        '#drop_zone.highlight {background:#fee;}',
        '</style>'
    ].join('');
    document.write(additionalStyle);


    var processFiles = function (event) {
        var element = event.target;
        event.stopPropagation();
        event.preventDefault();
        removeDropZoneClass(event);

        // FileList object of File objects
        var files = event.dataTransfer.files;
        for (var i = 0, f; f = files[i]; i++) {
            var reader = new FileReader();

            // closure to capture file info
            reader.onload = (function (file, index) {
                return function (e) {
                    var dataUri = e.target.result;
                    var img = renderImage(dataUri);
                    element.innerHTML = img;
                };
            })(f, i);

            // read file as data URI
            reader.readAsDataURL(f);
        }

    };

    var highlightDropZone = function (e) {
        e.stopPropagation();
        e.preventDefault();
        document.getElementById('drop_zone').classList.add('highlight');
    }

    var removeDropZoneClass = function (e) {
        document.getElementById('drop_zone').classList.remove('highlight');
    }

    function prepareDropZone(ctx) {
        if (ctx.inGridMode) {
            return;
        }
        // add event listeners if File API is supported
        var dropZone = document.getElementById('drop_zone');
        if (window.File &amp;amp;&amp;amp; window.FileReader &amp;amp;&amp;amp; window.FileList &amp;amp;&amp;amp; window.Blob) {
            dropZone.addEventListener('drop', processFiles, false);
            dropZone.addEventListener('dragover', highlightDropZone, false);
            dropZone.addEventListener('dragenter', highlightDropZone, false);
            dropZone.addEventListener('dragleave', removeDropZoneClass, false);
        } else {
            dropZone.innerHTML = 'The File APIs are not fully supported in this browser.';
            dropZone.className = 'highlight';
        }

        document.onpaste = function (event) {
            var items = (event.clipboardData 
                 || event.originalEvent.clipboardData).items;
            console.log(JSON.stringify(items)); // will give you the mime types
            var blob = items[0].getAsFile();
            var reader = new FileReader();
            reader.onload = function (e) {
                var dataUri = e.target.result;
                var img = renderImage(dataUri);
                dropZone.innerHTML = img;
            }; // data url!
            reader.readAsDataURL(blob);
        }
       
    };

    //http://stackoverflow.com/a/822486/632117
    //in display form we have 
    //to strip the html elements that are pasted in by SharePoint
    function strip(html) {
        var tmp = document.createElement('DIV');
        tmp.innerHTML = html;
        return tmp.textContent || tmp.innerText || '';
    }
    function renderImage(fieldValue) {
        if (fieldValue) {
            return ['<img style="max-width:100px;" src="',
                strip(fieldValue), '"/>'].join('');
        }
        return '';
    }
    
    function view(ctx, field) {
        //disable editing in Quick Edit mode
        if (ctx.inGridMode) {
            field.AllowGridEditing = false;
        }
        var fieldValue = ctx.CurrentItem[ctx.CurrentFieldSchema.Name];
        return renderImage(fieldValue);
    }
    
    function renderDropZone(fieldValue) {
        var img = renderImage(fieldValue);
        return ['Drop files here<div id="drop_zone"', img, '</div>'].join('');
    }

    function edit(ctx, field) {
        var formCtx = SPClientTemplates.Utility.GetFormContextForCurrentField(ctx);
        formCtx.registerGetValueCallback(formCtx.fieldName, function () {
            var dropZone = document.getElementById('drop_zone');
            var img = dropZone.getElementsByTagName('img');
            return img.length ? img[0].src : '';
        });
        var fieldValue = ctx.CurrentItem[ctx.CurrentFieldSchema.Name];
        return renderDropZone(fieldValue);
    }

    var overrideContext = {};
    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates.Fields = {
        'DragAndDropImage': {
            'View': view,
            'DisplayForm': view,
            'EditForm': edit,
            'NewForm': edit
        }
    };

    overrideContext.Templates.OnPostRender = prepareDropZone;

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext);
})();

Disabling a column in Quick Edit

jsgrid-015
In my project I have a column called Request Status. This column is not shown in any forms, meaning users should not edit, because it is controlled through the app. Nevertheless it is editable in the Quick Edit.

Yesterday I wrote about jsgrid in my blog. Now comes more. Today I’ll share a little practical solution how one can disable editing a field in Quick Edit.

The field is edited in jsgrid, but to disable it, we only have set the property called AllowGridEditing to false on our column (not even touching the heavy jsgrid api). We can do in the OnPreRender event in our Client Side Rendering (CSR) registration. Having the context object we have access to the Fields (ContextInfo.ListSchema.Field):

(function () {
    var overrideContext = {};
    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates.OnPreRender = function(ctx) {
    	var statusField = ctx.ListSchema.Field.filter(function(f) {
    		return f.Name === 'Request_x0020_Status';
    	});
    	if (statusField) {
    		statusField[0].AllowGridEditing = false;
    	}
    }
    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext);
})();

Another way is to implement the display form:

(function () {
    var view = function (ctx, field) {
        if (ctx.inGridMode) {
            field.AllowGridEditing = false;
        }
        return window.RenderFieldValueDefault(ctx);
    };
    var overrideContext = {};
    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates.Fields = {
        'Request_x0020_Status': {
            'View': view
        }
    };
    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext);
})();

JSGrid Basics

JSGrid is the javascript framework in SharePoint used in Quick Edit View (previously Datasheet View). There are a few very good blog posts on this topic (See below in “Sources”). Nevertheless the fact is that jsgrid and working with quick edit from a developer’s perspective is a huge undiscovered area. Articles I have seen are intended for advanced developers. The goal with my post today is to outline the very basics of working with JSGrid. When you know the basics you will be more comfortable to discover and try out more.

The example I want to show is a jsgrid code for a “VerySimpleColumn”. The source code can be found on gist.github.com: https://gist.github.com/mirontoli/838d60df76107fac56a0 To focus on jsgrid, I assume you have knowledge and some experience of working with jslink, which is related to jsgrid.

A word of caution before we start

JSGrid is an undocumented part of SharePoint javascript “ecosystem”, neither it is a part of the official SharePoint javascript api. So actually we should not use it. On the other hand JSGrid indicates something that is more like a full-fledged javascript api because:

  1. It seems to be carefully prepared (all possible situations are covered)
  2. The api is human-readable. The events and properties are called OnBeginEdit, OnEndEdit, Focus, BindToCell and so on (opposed to the properties in the SP.Microfeed.js like $v_1, $v_2)
  3. It follows many best-practices for handling the UI in javascript, eventhough some constructs are clumsy, e.g. using absolute-positioned overlays on top of original table cells while editing a cell value.

So to me it seems quite okay to use it in real applications, but we have to live with the risk that the jsgrid api will be changed without any notice to us developers.

Set up a site column with a custom jslink

The first step is to set up everything so we can start discovering jsgrid. It is just a walkthrough, not a detailed explanation. If you are eager to look at jsgrid, jump directly to that section. In this example we’ll apply a jslink to a custom site column. The very same jslink will be used in jsgrid, too.

First of all add a new Site Column, call it VerySimpleColumn. The type is Multiple lines of text:

jsgrid-001

Let us put it in a new group: Tolle Columns (beautiful, huh?)

jsgrid-002

 

Three lines of plain text. Nothing extravagant.

jsgrid-003

After that we can verify that the column exists. Fine.

jsgrid-004

 

Now we have to update the jslink property for the new column. Make it in an app:

jsgrid-005

Now add a custom list, call it “TolleList”:

jsgrid-006

 

In the List Settings add the new site column, then you’ll see this:

jsgrid-007

 

Now upload an empty js file to the Style Library, call it field.jslink.verysimplecolumn.js:

jsgrid-008

 

It goes so fast 🙂 Now it is time to implement some jslink code

Writing jslink

I’ve been thinking a while. What example could I use for that jslink? I want it to be a very simple example, so we don’t need to concentrate on actual rendering logic. You can see sophisticated examples that I listed below in “Sources”. Here we will be using this example: We’ll append a text to the field value: “This field is fully controlled by jslink”. This text should be visible on all forms, but it is not a part of the actual field value. Well here it is:

jsgrid-009

The code should appear simple for us who have written at least some jslink code.

(function () {
    function verySimpleView(ctx, field) {
        var wrapper = 'This field is fully controlled by jslink<hr><span>{0}</span>';
        var value = ctx.CurrentItem[ctx.CurrentFieldSchema.Name] || "";
        return String.format(wrapper, value);
    }
    
    function verySimpleNewAndEdit(ctx) {
        var wrapper = 'This field is fully controlled by jslink<br><input type="text" id="{0}" value"{1}"/>';
        var id = 'tolle-' + new Date().getTime();
        var formCtx = SPClientTemplates.Utility.GetFormContextForCurrentField(ctx);
 
        formCtx.registerGetValueCallback(formCtx.fieldName, function () {
            var input = document.getElementById(id);
            return input.value;
        });
        var value = ctx.CurrentItem[ctx.CurrentFieldSchema.Name] || "";
        var html = String.format(wrapper, id, value);
        return html;
    }
    var overrideContext = {};
 
    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates.Fields = {
        'VerySimpleColumn': {
            'View': verySimpleView,
            'DisplayForm': verySimpleView,
            'EditForm': verySimpleNewAndEdit,
            'NewForm': verySimpleNewAndEdit
        }
    };
 
    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext);
})();

This is how it looks like in the DispForm:

jsgrid-010

JSGrid. First Step: Disable the field in Quick Edit

Now we are ready to handle the Quick Edit. What we’ll do first is to disable editing our column. Why? Let’s say we have some complicated logic for rendering and editing of our field, we don’t the quick edit override it. So before we start implementing the QuickEdit part of the field, we want to disable it. Here, perhaps, ends the implementation of Quick Edit for some business cases.

jsgrid-011

To determine the Quick Edit we can use the propery of ther Render Context called inGridMode. The Quick Edit list uses the “View” template:

function handleGridMode(ctx, field) {
    field.AllowGridEditing = false;
}
function verySimpleView(ctx, field) {
    var wrapper = 'This field is fully controlled by jslink<hr><span>{0}</span>';
    var value = ctx.CurrentItem[ctx.CurrentFieldSchema.Name] || "";
    if (ctx.inGridMode) {
        handleGridMode(ctx, field);
    }
    return String.format(wrapper, value);
}

Wiring up a jsgrid EditControl

When our code works in all forms and the values are not “damaged” in the Quick Edit View, then the next step is to create an Edit Control that is used by the jsgrid “engine”. The very minimal Edit Control object is as follows. It contains a few event listeners and properties:

{
    SupportedWriteMode: SP.JsGrid.EditActorWriteType.LocalizedOnly,
    SupportedReadMode: SP.JsGrid.EditActorReadType.LocalizedOnly,
    BindToCell: function () { console.log("tolle BindToCell"); },
    OnCellMove: function () { console.log("tolle OnCellMove"); },
    Focus: function () { console.log("tolle Focus"); },
    OnBeginEdit: function () { console.log("tolle OnBeginEdit"); },
    OnEndEdit: function () { console.log("tolle OnEndEdit"); },
    Unbind: function () { console.log("tolle Unbind"); },
    Dispose: function () { console.log("tolle Dispose"); }
}

This edit control is created and returned in a function that is called: createVerySimpleColumnGridEditControl.

var createVerySimpleColumnGridEditControl = function (gridContext, cellControl) {
    return {
        SupportedWriteMode: SP.JsGrid.EditActorWriteType.LocalizedOnly,
        SupportedReadMode: SP.JsGrid.EditActorReadType.LocalizedOnly,
        BindToCell: function () { console.log("tolle BindToCell"); },
        OnCellMove: function () { console.log("tolle OnCellMove"); },
        Focus: function () { console.log("tolle Focus"); },
        OnBeginEdit: function () { console.log("tolle OnBeginEdit"); },
        OnEndEdit: function () { console.log("tolle OnEndEdit"); },
        Unbind: function () { console.log("tolle Unbind"); },
        Dispose: function () { console.log("tolle Dispose"); }
    }
};

Now we have to register our Edit Control when jsgrid is ready. We have to write a callback function and invoke SP.GanttControl.WaitForGanttCreation. Inside the callback function we register our Edit Control for our field: SP.JsGrid.PropertyType.Utils.RegisterEditControl

function handleGridMode(ctx, field) {
    window.SP.SOD.executeOrDelayUntilScriptLoaded(function () {
        window.SP.GanttControl.WaitForGanttCreation(function (ganttChart) {
            var verySimpleColumn = null;
            var editId = "EDIT_TOLLE_VERYSIMPLEFIELD";
            var columns = ganttChart.get_Columns();

            for (var i = 0; i < columns.length; i++) {
                if (columns[i].columnKey == "VerySimpleColumn") {
                    verySimpleColumn = columns[i];
                    break;
                }
            }
            if (verySimpleColumn) {
                verySimpleColumn.fnGetEditControlName = function (record, fieldKey) {
                    return editId;
                };

                window.SP.JsGrid.PropertyType.Utils.RegisterEditControl(editId, function (gridContext, cellControl) {
                    return createVerySimpleColumnGridEditControl(gridContext, cellControl);
                }, []);
            }

        });
    }, "spgantt.js");
}

In our first version of the Edit Control we only log the event names to the web browser console. It works.

jsgrid-012

Implementing the actual editing

When working on this I did small changes to these events and tried it out in the web browser. I’d recommend it to you, too. Trying out is the best way of learning. Here comes the functions that we need to have to implement:

  • createVerySimpleColumnGridEditControl (“constructor”). Here we initialize the edit control
  • bindToCell. Here we get the cellContext that we save as a “private” variable on that Edit Control object. The cell context is needed to get and set the field value.
  • focus. Here we define what element should be focused. In this case we forward the focus event to the actual input
  • onBeginEdit. Here we show the Edit Control and make it editable.
  • onEndEdit. Here we save the field value and hide the edit control.

jsgrid-013

Some notes on the Edit Control and Events

The Edit Control has a “container” – a html element that contains the representation of the field in edit mode. The Edit Control Container is an overlay – an absolutely-positioned element that exists outside the actual listview. We must set the position and we are responsible for hiding it when the field is not edited:

container.style.cssText = 'visibility:hidden;position:absolute;top:0;left:0;background:#ffffff;';

We also need to set the dimensions of the edit control container. It is quite easy using the information from cellContext:

var bindToCell = function(ctx) {
    cellContext = ctx;
    //An input is put as an overlay. 
    //We have to set the width and height so that it takes the whole cell place
    container.style.minWidth = cellContext.cellWidth + 'px';
    container.style.width = cellContext.cellWidth + 'px';
    container.style.height = cellContext.cellHeight + 'px';
    console.log("tolle BindToCell");
};

In OnBeginEdit we have to show the container, and in OnEndEdit we have to hide it

//OnBeginEdit
cellContext.Show(container);
//OnEndEdit
cellContext.Hide(container);

We have to save the value using the cellContext.

var value = input.value;
cellContext.SetCurrentValue({
    localized: value
});

Final code

(function () {
    var editWrapper = '<span>This field is fully controlled by jslink</span><br><input type="text" id="{0}" value"{1}"/>';
    var createVerySimpleColumnGridEditControl = function (gridContext, gridTextInputElement) {
        var cellContext, inEdit, html, container, input, id;
        id = "tolle-" + new Date().getTime();
        html = String.format(editWrapper, id, "");
        container = document.createElement("div");
        container.innerHTML = html;
        input = container.getElementsByTagName("input")[0];
        container.style.cssText = 'visibility:hidden;position:absolute;top:0;left:0;background:#ffffff;';
        gridContext.parentNode.appendChild(container);
        var bindToCell = function(ctx) {
            cellContext = ctx;
            //An input is put as an overlay. We have to set the width and height so that it takes the whole cell place
            container.style.minWidth = cellContext.cellWidth + 'px';
            container.style.width = cellContext.cellWidth + 'px';
            container.style.height = cellContext.cellHeight + 'px';
            console.log("tolle BindToCell");
        };
        var onCellMove = function () { console.log("tolle OnCellMove"); };
        var focus = function(eventInfo) {
            input.focus();
            console.log("tolle Focus", eventInfo);
        };
        var onBeginEdit = function (eventInfo) {
            inEdit = true;
            var currentValue = cellContext.originalValue.localized;
            if (currentValue) {
                input.value = currentValue;
            }
            cellContext.Show(container);
            console.log("tolle OnBeginEdit");
            this.Focus(eventInfo);
        };
        var onEndEdit = function() {
            cellContext.Hide(container);
            inEdit = false;
            var value = input.value;
            cellContext.SetCurrentValue({
                localized: value
            });
            console.log("tolle OnEndEdit");
        };

        var unbind = function() { console.log("tolle Unbind"); };
        var dispose = function () { console.log("tolle Dispose"); }

        return {
            SupportedWriteMode: window.SP.JsGrid.EditActorWriteType.LocalizedOnly,
            SupportedReadMode: window.SP.JsGrid.EditActorReadType.LocalizedOnly,
            BindToCell: bindToCell,
            OnCellMove: onCellMove,
            Focus: focus,
            OnBeginEdit: onBeginEdit,
            OnEndEdit: onEndEdit,
            Unbind: unbind,
            Dispose: dispose
        }
    };

    function handleGridMode(ctx, field) {
        window.SP.SOD.executeOrDelayUntilScriptLoaded(function () {
            window.SP.GanttControl.WaitForGanttCreation(function (ganttChart) {
                var verySimpleColumn = null;
                var editId = "EDIT_TOLLE_VERYSIMPLEFIELD";
                var columns = ganttChart.get_Columns();

                for (var i = 0; i < columns.length; i++) {
                    if (columns[i].columnKey == "VerySimpleColumn") {
                        verySimpleColumn = columns[i];
                        break;
                    }
                }
                if (verySimpleColumn) {
                    verySimpleColumn.fnGetEditControlName = function (record, fieldKey) {
                        return editId;
                    };

                    window.SP.JsGrid.PropertyType.Utils.RegisterEditControl(editId, function (gridContext, cellControl) {
                        return createVerySimpleColumnGridEditControl(gridContext, cellControl);
                    }, []);
                }

            });
        }, "spgantt.js");
    }


    function verySimpleView(ctx, field) {
        var wrapper = 'This field is fully controlled by jslink<hr><span>{0}</span>';
        var value = ctx.CurrentItem[ctx.CurrentFieldSchema.Name] || "";
        if (ctx.inGridMode) {
            handleGridMode(ctx, field);
        }
        return String.format(wrapper, value);
    }
    
    function verySimpleNewAndEdit(ctx) {
        var id = 'tolle-' + new Date().getTime();
        var formCtx = window.SPClientTemplates.Utility.GetFormContextForCurrentField(ctx);

        formCtx.registerGetValueCallback(formCtx.fieldName, function () {
            var input = document.getElementById(id);
            return input.value;
        });
        var value = ctx.CurrentItem[ctx.CurrentFieldSchema.Name] || "";
        var html = String.format(editWrapper, id, value);
        return html;
    }
    var overrideContext = {};

    overrideContext.Templates = overrideContext.Templates || {};
    overrideContext.Templates.Fields = {
        'VerySimpleColumn': {
            'View': verySimpleView,
            'DisplayForm': verySimpleView,
            'EditForm': verySimpleNewAndEdit,
            'NewForm': verySimpleNewAndEdit
        }
    };

    window.SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideContext);
})();


Summary

Well, jsgrid is quite complicated. I have tried to keep it as simple and minimalistic as possible. When we can these basics, we can move on and master advanced examples where we can deliver high value to business that loves Quick Edit 🙂

Sources

Interesting, Anton, Andrey and me are born in Sovyet and are interested in JSGrid. Coincidence? 🙂

Improving the web performance of an intranet

overloaded-sharepoint-in-browser

All the “small” app parts, web parts, delegate controls, user controls, and other “packages” that “must” be delivered to the users on every page load of the Start Page of your Intranet.

Recently we made an investment to improve the performance of our intranet. We made many changes in different layers: SQL, Network, Browser upgrade and code. Here I want to tell about what code changes we did to improve the web browser performance. Please leave feedback if you find it useful. You can also share your suggestions. We measured the performance before our code changes and after them. We had amazing results. Unfortunately I can not share any numbers, but we improved the Time to First Byte, time to load event firing in the browser, memory consumption in the clients and, perhaps, the most important, we improved the perceived performance of the Intranet, the way how users experience the speed and UI responsiveness. To do this I got many ideas of my project colleagues and branch colleagues. Here is the list of changes we’ve implemented:

– Execute code on demand

Consider this scenario: on a page users can click on a button to download a vcard. Aggregating user information is a costly operation that requires getting data from the User Profile Service, getting the profile image from SharePoint. Don’t ever do this operation on page load. Move the code to the “onclick” action. In other words, work when it is needed. It is like cooking a lot of food, when you are not hungry. It is nothing new, unfortunately there were a couple of the “eager code” places.

– Cache results, investigate what parts can be cached and how fresh data needs to be

All data on your page doesn’t need to be fetched on every page load. In our project We listed all the “parts” of the start page and other often visited pages and went to the business and asked them to evaluate how fresh data should be. Some parts should be as fresh as possible (no cache), whereas it would be tolerated that the information could be dirty (cache up to one week or more).

– Reference javascript and css files from one location

Do you have jQuery in your SharePoint Intranet. How many copies do you have? What we did in our Intranet was that we partially implemented the CDN concept. Even though we don’t distribute our resource files geographically, we have 1-to-1 relation between a file and its absolute URL. In the whole intranet, we have only one jQuery url and only one our intranet.core.js url. We did by creating a dedicated CDN site collection. This alone makes a big difference. To evolve the idea we could provision resources outside SharePoint to remove the authorization overhead. We could also distribute it geographically by having files closer to the end users.

– Unify your framework and your dependencies

In our projects we had a couple of SharePoint-hosted apps developed by different teams. We had different approaches and different framework that solved almost the same problem: AngularJS and KnockoutJS. Eventhough apps are independent pieces of software, they were used within the same page (as app parts). It was too much http traffic. We agreed on Developer Guidelines and chose one framework.

– Do not hide controls on the page with CSS, remove them

On the start page in our intranet we didn’t show the left navigation, but it was still rendered in code behind. Instead hiding it with CSS, we just removed it by an empty ContenPlaceHolder in our Start Page Layout:


<asp:Content ContentPlaceholderId="PlaceHolderLeftNavBar" runat="server"/>

– Optimize jQuery Selectors

We reviewed all the jQuery code and improved the selectors. Optimizing the selectors will improve the overall performance in the browser, especially in older browsers. The worst example is using text selectors, like this one:

jQuery('#NoteBoardContainer*:contains("There are no notes posted yet. You can use notes to comment on a page"):last');

It will sink your IE8 browser.

– Minify javascript and CSS files

Minifying resource files like javascript and css is not hard. My recommendation is to use Web Essentials plugin in Visual Studio. Alternatively you can use the SharePoint Assets Minifier.

– Use the weakest selectors in CSS and in LESS

In our project we are using LESS. With LESS it is easier to write  readable CSS code. But be aware of the output. Do not make the selectors too strong: Use the weakest CSS Selectors. The weakest selectors will make it easier to maintain the CSS and it will minimize the amount of KB the server needs to send to your users’ browsers.

– Ensure javascript and CSS files are cached

JavaScript and CSS files should be cached. You should also avoid 304 responses where the Server answers “Not Modified”, because this has an impact on the performance. Configure the Blob Cache and put your resources into the Style Library.

– Remove all app parts from SharePoint-hosted Apps from the start page

There can be exceptions, but we encountered that client web parts (app parts) from SharePoint-hosted apps had a huge impact on the performance. The combination of a couple of app parts on often visited pages (like the start page on the intranet) led to long page load times. These are the reasons why you should not have SharePoint Hosted App Parts on your start page:

  1. App Parts are iframes. They are loaded simultaneously if you add client web parts (app parts) in a usual way. They hold up the whole page. Users cannot interact with the intranet page until all the content in all app parts has been loaded. This can be partially improved if you introduce a delay in the app part loading, by developing an own engine. See my blog post where I mention such a concept: AppLoader Concept for SharePoint apps.
  2. The content from a SharePoint AppWeb is not fully cached. If you examine the http traffic from the apps you’ll see a lot of 304 responses, meaning the browser requests when the server answers that there is no newer version. This has an impact on the performance. See more in Alik Levin’s blog: ASP.NET Performance: Get Rid of HTTP 401 and HTTP 304. In a SharePoint-hosted app you don’t have any control what so ever to adjust the the cache settings. This is not the case in the Provider-hosted apps.
  3. SharePoint-hosted apps can only use javascript. The code is executed on the client. Older browsers like IE9 or IE8 render the pages slower. The Start Page that is slower than the rest of the Intranet is not something that will engage your users.
  4. App Parts are iframes that do not know about their dimensions. App parts often need to update the height and the width  of the parent iframe. This causes irritating flickering. Perhaps OK on some pages, but I’d say totally unacceptable on the start page of your brand new intranet..

What did we do instead of App Parts on the Start Page? We converted them into Script Editor Web Parts, the app parts were only one-time parts, they only were used on the start page.

Do not get me wrong. What we did was not abandoning apps as a model, we just removed wrong apps, apps that cannot be reused, the SharePoint-hosted apps that had big performance issues. I am looking forward creating right apps, that are written with performance, reusability, scalability and good design in mind.

 

AppLoader Concept for SharePoint apps

In this post I want to share an unusual, nevertheless interesting conceptual idea of loading content from SharePoint 2013 apps on many pages. The original awesome concept was proposed and developed by my colleague Martin Villysson at Bool.

The problem we are trying to solve

SharePoint apps are great to extend functionality in SharePoint and integrate other systems (full page apps available through Site Contents), they also provide tools to enrich the default SharePoint experience by App Parts (Client Web Parts) and Custom Actions (additional menus).

One of the biggest shortcomings of that model is the need to add app parts on all pages where it is needed. Let’s say, we want to have some app parts present on every single page in our whole SharePoint tenancy, to provide a consistent look and feel (e.g. navigation, notifications). Traditionally, on premises, we have added user controls in our customized master page. In SharePoint Online that is impossible. The complicated workaround is to add those client web parts (app parts) on every page, be it manually or by automating it (powershell or app). It will require updating all pages. Nevertheless, it will not work on Out-of-the-box application pages (pages from layouts folder). It becomes even more unacceptable when you realize that your app must be added as an app instance on every single site (SPWeb) in your tenancy. 

Towards a solution

Allright, we don’t want to have thousands of app instances of the same app. What we can do is to use Tenant scoped apps (Tenant Apps). Then we’ll need only one app instance. But wait, app parts from a tenant app are only available in the parent site (HostWeb), meaning – App Catalog. That’s not good. So what Martin found in the SharePoint internal javascript code is using of _layouts/15/TenantAppInfo.ashx, a http handler that provides information about all Tenant Apps and their custom actions.That’s how the idea of the AppLoader was born.

Vesa Juvonen

After we had created a working Proof-of-Concept of the AppLoader concept, I met Vesa Juvonen at the SharePoint Conference in Las Vegas and introduced this idea to him (although I didn’t call it AppLoader). He liked it although he pointed out that this TenantAppInfo.ashx is an internal utility only in SharePoint and it is not supported by Microsoft. That’s correct. There is even almost no information about it on the Internet. But I got a feeling of Microsoft that they are willing to hear feedback and improve the product. Vesa encouraged me also to blog about it. So now I am telling about this idea. I hope to hear feedback about it. Unfortunately I cannot share the source code of the working Proof-of-Concept solution.

AppLoader Concept in colors

The AppLoader Concept is quite simple. Look at this picture:

apploader-concept

 

The solution contains a custom Master Page (blue) that references a javascript file called apploader.js (red). This file initializes the whole process. Tenant Apps (green) are the apps that an administrator has installed in the App Catalog and deployed to the whole tenancy. TenantAppInfo.ashx (black) is a handy but officially unsupported OOB service utility (http handler) that returns a json-formatted list of all Tenant Apps (green). AppLoader (red) receives the app list (black) and renders it on the Page (blue) inside new iframes (red). The page a user has navigated to can be any page (wiki page, publishing page, application page, really any page).

To summarize the colors in the diagram: red is our javascript code, green are all the tenant apps and their content, black is the utility and its output, blue is a sharepoint page and its underlying component (master page).

The steps in the AppLoader process:

  1. Make an ajax request to TenantAppInfo.ashx using XHR (XmlHttpRequest)
  2. Receive the app list
  3. For every app information, render app part, or inject css and javascript references. 

 

Reading what to render on the page

You probably have already have tried to navigate to _layouts/15/TenanAppInfo.ashx while reading this post, I know you are curious. Then you’ve noticed that there is no information about app parts. So you may ask: how do we know what app parts to render and where to put them in the page, and how do we know what resources (css and javascript files) to inject on the page. Well there is no information about it in the apps list. But if you have an app with custom actions you’ll see that they are listed in this json-formatted list we receive from the TenantAppInfo.ashx. So the solution is the brilliant idea of my colleage Martin to define custom actions in the app. CustomActions contain a ActionUrl. The ActionUrl points to the url to render (app part page) or to inject (javascript or css file). The apploader.js reads the ActionUrl in the Custom Actions for every app information and takes action upon it (rendering an app part iframe, or injecting a javascript or css file). That’s it. 

Usage and Limitations

This bright idea takes advantages from a huge SharePoint API (that contains a lot of good but not supported parts) to make using of apps in Client Application Model solutions more pragmatic and still provide a consistent design and behavior. By consistent design I mean same parts like additional navigation, notifications etc in the whole Intranet. The AppLoader renders and injects whatever you have rolled out to your whole tenancy (Tenant Scoped Apps) and that on every page (!). It also improves the perceived performance of the page load, because it renders app parts (iframes) after the main page has been loaded preventing freezing of the page. 

There are of course some limitations in the AppLoader Concept. Today we cannot rely on the TenanAppInfo.ashx API (because it is not supported and future updates can break solutions). We have to define our own custom actions in the apps. That means we can only use our own apps, it will hardly work with the apps installed from the Office Store. On the other hand, your customer will not want to have generic apps from the Office Store to be a part of every page on their intranet.

Using CAML with SharePoint REST API

Do you prefer REST over CSOM as I do? I’ll skip the whys. Andrew Connell put it already in wrtiting so nicely. Well, if you do prefer REST, then you must have discovered some shortcomings of REST, or its incompleteness compared to CSOM. I think of:

  1. Inability to filter items based on multivalued taxonomy fields
  2. Inability to filter items based on user fields where user is added through a group, rather than directly, e.g. AssignedTo=[Me] combined with a SharePoint group.

In such situations I was forced to use CSOM. Until yesterday. Yesterday I learned that we can actually use CAML queries in REST requests.

 

This enables using REST in all situations. The REST API is still developed and many features are added. Maybe a particular operation that needs a CAML query today, can be supported in the core REST API and can be easily refactored then.

But until then, we can use CAML queries in REST requests. Here are the important things about it:

  • A REST request with a CAML query is always a POST request
  • A REST request with a CAML query has always to have X-RequestDigest http header (actually because it is a POST request)
  • A REST request with a CAML query should always have the attached CAML query in the request body (and not in a query string). We don’t want to mess with long urls, do we?
  • A REST request with a CAML query must have the http header “Content-Type: application/json;odata=verbose” unless you use xml in the request body.
Needed HTTP Headers in REST requests

HTTP headers you have to provide in REST requests with CAML queries

You can use jQuery or SP.RequestExecutor to make an ajax call. The REST endpoint is:

_api/web/Lists/GetByTitle('<your list title>')/GetItems

The request body (if you use json, and I bet, you do) is in this format:

{ "query" :
   {"__metadata": 
      { "type": "SP.CamlQuery" }
      , "ViewXml": "<YOUR CAML QUERY>" 
   }
}

Here is the boilerplate for a REST request with a CAML Query:

function getDataWithCaml(listName, caml) {
    var endpoint = "/_api/web/lists/GetByTitle('" 
        + listName + "')/GetItems";
    var requestData = { "query" :
           {"__metadata": 
              { "type": "SP.CamlQuery" }
              , "ViewXml": caml
           }
        };
    return jQuery.ajax({
        url: endpoint,
        method: "POST",
        data: requestData,
        headers: {
            "X-RequestDigest": $("#__REQUESTDIGEST").val(),
            "Accept": "application/json; odata=verbose",
            "Content-Type": "application/json; odata=verbose"
        }
    });
}

This function is just an example. It has no error handling, and it takes for granted that your list is in the root site for on your (sub-)domain (“/”). So take it as an example only.

Here is how the function can be invoked

var caml = "<View><Query><Where><Or><Eq><FieldRef Name='AssignedTo' /><Value Type='Integer'><UserID/></Value></Eq><Membership Type='CurrentUserGroups'><FieldRef Name='AssignedTo' /> </Membership></Or></Where></Query></View>";
getDataWithCaml("Tasks", caml);

Pragmatic Responsive Design

I have been curious about the responsive design but have not had time to try it out. To learn more I decided to make an existing website more responsive. A friend of mine drives a Chuvash Dictionary website: samah.chv.su. Today it looks like this in a mobile browser:

Before responsiveThe site is a classic 1000px-ish centered page with header and two columns. The left column is for the main content and the right column for additional “aside” information. Can it be more classic? This current version works, you can still use the dictionary on a mobile phone. But there are several improvements that can be done:

  • Avoid scaling to be able to read text
  • Avoid scrolling back and forth to read every line
  • Move the right column down, it is better to use the space for the main content.
  • Hide the quick links to the individual letters
  • Shrink unused space in the header.



What I wanted was to provide some easy steps to make it more responsive. The steps had to be pragmatic: some easy-to-implement steps that would make a difference, and with very little impact on existing markup.

I created a copy of this page and made it available publicly, because I wanted to access from everywhere and test it on so many devices and resolutions as possible. I used Github Pages to get the version control too, even though a public folder on Dropbox, OneDrive or Google Drive could give the same result.

Here is my list of what I did to make it more responsive:

Don’t do anything for larger resolutions

Just leave it as it is, unless you want to redesign your site.

Create the css file for responsive design

Just create a file and call it responsive.css. Reference this file inside your site in a usual way.

<link rel="stylesheet" href="./responsive.css" type="text/css">

Set witdh 100% when a scroll appears

Find the “break point” where your site gets a horizontal scroll. To do so, just fire the Chrome Dev Tools, dock it to the right, and resize your page. In my case it was 1016px. Now it is time to do a change. Create a media query for that and try to remove all the hard-coded width values by setting width:100%. Well something towards this “very responsive site”.

@media all and (max-width: 1016px) {
    .art-sheet {
        width: 100%;
    }
    div.art-header {
        width: 100%;
    }
}

Let the columns disappear

Find the width where two columns become ugly and impractical. Create a media query for that and just style the columns so they do not float. In my case the original site uses table-row layout. I set display:block and the right column went down.

@media all and (max-width: 675px) {
  div.art-content-layout-row {
      display: block;
  }
  div.art-content-layout div.art-layout-cell {
      display: block;
  }
  div.art-content-layout-row {
      display: block;
  }
}

Prevent scaling in mobiles

Now your page is more responsive in a desktop browser, but it is still unreadable in a mobile. The reason is the mobile browser that scales it for us. Let’s disable it. Just put this meta tag in the head session of your page:

<meta name="viewport" content="width=device-width, initial-scale=1">

Hide rarely used elements

In a mobile we want to see the most important things, the actual content. Hide menus and remove empty spaces. In my case I hid the letters that took to much place and the language switcher. They can be shown in another menu (see below).

Make a responsive navigation menu

It is not complicated at all. You can google it. There are many tips and jQuery plugins. I did in a very easy way. I added a new div to my html:

<div id="responsive-menu" onclick="toggleMenu()">☰</div>

I also added this css style and hid this div for bigger resolutions:

#responsive-menu {
  position: absolute;
  right: 5px;
  top: 15px;
  font-size: 30px;
  cursor: pointer;
  display: none;
}
@media all and (max-width: 399px) {
  #responsive-menu {
    display: block;
  }
}

Here is the tiny javascript click event listener:

(function() {
  var menuOpen = false
      , menu
      , langSwitcher;

  window.toggleMenu = function() {
    menu = menu || document.getElementsByTagName("center")[0];
    menu.style.display = menuOpen ? "none" : "block";
    menuOpen = !menuOpen;
  }
})();

Here is the result. There are still some improvements that can be done, but here is the result of the my changes.

samah-004 samah-003

Add a website icon

Improve the user experience by adding a website icon. It will be used when an iPhone, iPad and Android user will save your page as a bookmark (and it will be placed on the screen and look like any app). Here is how I did:

  1. I created a 74 x 74 px png image and saved it as apple-touch-icon.png. I placed it in the “root folder”.
  2. In the page markup I added this line inside the head element:
<link rel="apple-touch-icon" href="apple-touch-icon.png">

That’s it. So now when someone saves the website as a bookmark, it looks like this:

photo samahsar-001 Screenshot_2014-03-25-23-30-02

Then on the mobile screen it looks like any other app. It works even on Android devices (even though the icon is an apple-touch-icon).

 

UPDATE 2014-03-25

Now these changes are implemented on the real site: samah.chv.su

UPDATE 2014-05-12

I discovered a blog post about how to adjust a web app for best experience in iOS. This gist is a perfect example what you can add for properties.

<!-- Run in full-screen mode. -->
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- Make the status bar black with white text. -->
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<!-- Prevent text size adjustment on orientation change. -->
<style>html { -webkit-text-size-adjust: 100%; }</style>

Count lines of code with PowerShell

Today I got a question:

How many lines of code are there in our SharePoint solution?

After a little search, I found that PowerShell is really a nice tool to count lines of code:

I wanted to count lines for different types of code:

  1. Code Behind written in C#, the files have .cs file extension
  2. JavaScript code (except jQuery, angular or knockout frameworks)
  3. PowerShell files (.ps1 and psm1)
  4. Xml files (all the SharePoint .xml files)

Here is the powershell code that counts lines of code:

# go to the solution folder
cd <solution directory>

#count lines in .cs files
ls -include *.cs -recurse | select-string . | measure | select count

#count lines in our .js files
ls -include *.js -recurse `
    -exclude *min.js, jquery*, _*, jsrender*, CamlBuilder*, knockout* `
  | select-string . `
  | measure `
  | select Count

#count lines in our powershell scripts
ls -include *.xml -recurse | select-string . | measure | select count

#count lines in our powershell scripts
ls -include *.ps1, *.psm1 -recurse | select-string . | measure | select count

Just a curious fact, I can’t tell you how many lines of code we have in our solution, but I can reveal the proportions. If I used the flexible box model in css3, it would look like this:

lines of code

There are as many lines of code written in javascript as it is in C#. The main reason that for the big js code base are the SharePoint hosted apps. The PowerShell scripts are as big the javascript code base. Xml files are 4 times bigger than C# code, and it is even bigger than the sum of all lines of code written in C#, JavaScript and PowerShell. It isn’t strange that xml is dominating, almost everything in SharePoint is defined in xml.

Fortunately, there are less cases where you have to write raw xml in Visual Studio 2012/2013 and SharePoint 2013.

How does it look in your project? What language is dominating in your SharePoint project?

Debugging OOB SharePoint. Unable to post comments on SharePoint blogs (SP2013 June CU)

I have had a strange bug. The comment text box in a OOB SharePoint 2013 blog doesn’t appear. It only says: “There are no comments for this post.” In this blog post I’ll tell you how I found the bug and I’ll show you how you can temporarily bring the commenting to life.

comments-not-working-001

I have had luck. While it doesn’t work on the Test Environment, it does actually work on my development machine. The comments text box is rendered as an OnPostRender action in the blog comments display template. After debugging the javascript in hours and comparing the two environments, I just could confirm that displaytemplates are the same. There are two main javascript files that are involved:

  • clienttemplates.js
  • sp.ui.blogs.js

So it must some differences elsewhere. The build versions of the environments are

  • Working Environment:  15.0.4420.1017 (SharePoint 2013 RTM)
  • Not-working Environment: 15.0.4517.1005 (SharePoint 2013 June CU)

No errors are thrown, even on the blog where it doesn’t work. It is because of the permissive try-catch in this function that wraps rendering of the comment text box:

function CallFunctionWithErrorHandling(fn, c, erv, execCtx) {
    if (SPClientRenderer.IsDebugMode(c)) {
        return fn();
    }
    try {
        return fn();
    }
    catch (e) {
        if (c.Errors == null)
            c.Errors = [];
        try {
            e.ExecutionContext = execCtx;
            if (Boolean(SPClientRenderer.AddCallStackInfoToErrors) && typeof execCtx == "object" && null != execCtx) {
                execCtx.CallStack = ULSGetCallstack(CallFunctionWithErrorHandling.caller);
            }
        }
        catch (ignoreErr) { }
        c.Errors.push(e);
        return erv;
    }
}

This is where the error occurs but doesn’t appear in the script console. Here is the error:

ExecutionContext: Object
message: "Syntax error, unrecognized expression: #{B808453A-141A-4091-8467-B6915B8B3E99}-{6C3BF638-43F2-4F12-A297-DEF18635540E}-CommentContainer"
stack: "Error: Syntax error, unrecognized expression: #{B808453A-141A-4091-8467-B6915B8B3E99}-{6C3BF638-43F2-4F12-A297-DEF18635540E}-CommentContainer
    at Error ()
    at Function.st.error (jquery-1.9.1.min.js:4:10926)
    at ft (jquery-1.9.1.min.js:4:17138)
    at wt (jquery-1.9.1.min.js:4:19823)
    at Function.st (jquery-1.9.1.min.js:4:6140)
    at b.fn.extend.find (jquery-1.9.1.min.js:4:20941)
    at b.fn.b.init (jquery-1.9.1.min.js:3:1126)
    at b (jquery-1.9.1.min.js:3:206)
    at /ScriptResource.axd:344:53
    at _foreach (ScriptResource.axd:33:17)"

What? jQuery error? Why?

It turns out that jQuery is used to get the html element for the comments area in the server with June CU, but not in the Server with SharePoint 2013 RTM.

To get the reference to the comments area, it invokes the $get method, which invokes Sys.get which in some cases invokes the jQuery (if present)

$get implementation in SP 2013 June CU

These are the functions which can be found in the SharePoint 2013 June CU (15.0.4517.1005)

$get = $type.getElementById = function DomElement$getElementById(id, element) {
    /// <summary locid="M:J#Sys.UI.DomElement.getElementById">Finds an element by id.</summary>
    /// <param name="id" type="String">The id of the element to find.</param>
    /// <param name="element" domElement="true" optional="true" mayBeNull="true"></param>
    /// <returns domElement="true" mayBeNull="true">The element, or null if it was not found.</returns>
    var e = Function._validateParams(arguments, [
        {name: "id", type: String},
        {name: "element", mayBeNull: true, domElement: true, optional: true}
    ]);
    if (e) throw e;
    return Sys.get("#" + id, element || null);
}

 

        get: function get(selector, context) {
            /// <summary>Queries the DOM for a single DOM element.</summary>
            /// <validationOptions enabled="false" />
            /// <param name="selector">
            /// Selector for a DOM element based on id (#&lt;id>), class (.&lt;name>), or tag name (&lt;tagname>).
            //// More complex selectors may be used if jQuery is loaded.
            /// If multiple elements match the selector, the first one is returned.
            /// </param>
            /// <param name="context" optional="true" mayBeNull="true">An element, array of elements, or Sys.UI.TemplateContext to restrict the query within.</param>
            /// <returns>The matching element, or null if none match.</returns>
            return (context && typeof(context.get) === "function") ?
                context.get(selector) :
                this._find(selector, context, true);
        },

 

_find: function _find(selector, context, single, filter) {
    ...
    else if (window.jQuery) {
        if (!filter) {
            found.push.apply(found, jQuery(selector, context).get());
        }
        if (includeSelf) {
            found.push.apply(found, jQuery(context).filter(selector).get());
        }
    }
    ...
},

older (but working) $get implementation in SharePoint 2013 RTM

These are two versions which can be found in the older SharePoint Release: RTM (15.0.4420.1017)

var $get = Sys.UI.DomElement.getElementById = function Sys$UI$DomElement$getElementById(id, element) {
    /// <summary locid="M:J#Sys.UI.DomElement.getElementById" />
    /// <param name="id" type="String"></param>
    /// <param name="element" domElement="true" optional="true" mayBeNull="true"></param>
    /// <returns domElement="true" mayBeNull="true"></returns>
    var e = Function._validateParams(arguments, [
        {name: "id", type: String},
        {name: "element", mayBeNull: true, domElement: true, optional: true}
    ]);
    if (e) throw e;
    if (!element) return document.getElementById(id);
    if (element.getElementById) return element.getElementById(id);
    var nodeQueue = [];
    var childNodes = element.childNodes;
    for (var i = 0; i < childNodes.length; i++) {
        var node = childNodes[i];
        if (node.nodeType == 1) {
            nodeQueue[nodeQueue.length] = node;
        }
    }
    while (nodeQueue.length) {
        node = nodeQueue.shift();
        if (node.id == id) {
            return node;
        }
        childNodes = node.childNodes;
        for (i = 0; i < childNodes.length; i++) {
            node = childNodes[i];
            if (node.nodeType == 1) {
                nodeQueue[nodeQueue.length] = node;
            }
        }
    }
    return null;
}

It doesn’t even go to Sys.get and doesn’t even try to use jQuery.

The id of the comment are is a combination of two guids and -CommentContainer. jQuery fails to select an element with this id:
This error happens when you try to select this html element with jQuery in the web console:

comments-not-working-002

Temporary solution

The solution is to prevent using jQuery for this element selection. Hopefully new updates of SharePoint will address this issue. Until then we have to tweak the $get method. To do it very carefully, we can copy the old $get function and call it $get_asFoundInRTM and copy the new $get and call it $get_asFoundInJuneCU

The $get function has to rewritten like that:

$get_asFoundInJuneCU = $get

$get = function () {
    var id = arguments[0];
    return /\{/.test(id)
        ? $get_asFoundInRTM.apply(null, arguments)
        : $get_asFoundInJuneCU.apply(null, arguments);
}

This javascript code has to be loaded after two ScriptResource.axd files. Please be very careful if you have to implement this temporary fix. Probably it is better to wait for an official Microsoft bug fix.

 

UPDATE 2014-03-27

I have discovered that in SP1 it is already solved.

Вула Чăвашла

VulaCV - Чăвашла вулаттаракан сайт

Discovering SharePoint

And going crazy doing it

Bram de Jager - Coder, Speaker, Author

Office 365, SharePoint and Azure

SharePoint Dragons

Nikander & Margriet on SharePoint

Paul J. Swider - RealActivity

RealActivity is a specialized healthcare services and solution advisory firm.

Mai Omar Desouki - Avid SharePointer

Egyptian & Vodafoner - Senior SharePoint Consultant

Cameron Dwyer | Office 365, SharePoint, Outlook, OnePlace Solutions

Office 365, SharePoint, OnePlace Solutions & Life's Other Little Wonders

paul.tavares

Me and My doings!

Share SharePoint Points !

By Mohit Vashishtha

Jimmy Janlén "Den Scrummande Konsulten"

Erfarenheter, synpunkter och raljerande om Scrum från Jimmy Janlén

Aryan Nava

| Blockchain | Serverless Architecture | Microservices Architecture | DevOps | AWS Lambda | Teraform |

SPJoel

SharePoint for everyone

SharePointRyan

Ryan Dennis is a SharePoint Solution Architect with a passion for SharePoint and PowerShell

SharePoint 2020

The Vision for a Future of Clarity

Aharoni in Unicode

Treacle tarts for great justice

... And All That JS

JavaScript, Web Apps and SharePoint

blksthl

Mostly what I know about SharePoint - CommunicoCuspis