CHUVASH.eu

CHunky Universe of Vigourous Astonishing SHarepoint :)

Category Archives: javascript

Update Field.JSLink using JSOM or REST

Today I have just a little code snippet to share. This code snippet shows how to update the JSLink property for an existing field using JSOM and REST. For REST I use sharepoint-utilities.

var updateJsLinkCsom = function(config) {
	var ctx = SP.ClientContext.get_current();
	var web = ctx.get_web();
	var lists = web.get_lists();
	var list = lists.getByTitle(config.listTitle)
	var fields = list.get_fields();
	var field = fields.getByInternalNameOrTitle(config.fieldTitle)
	field.set_jsLink(config.jsLink)
	field.update()
	ctx.executeQueryAsync()
};
var updateJsLinkRest = function(config) {
	SP.SOD.registerSod('sputils.js', '<your cdn endpoint>/sputils.min.js')
	SP.SOD.executeFunc('sputils.js', '', function() {
		var url = _spPageContextInfo.webAbsoluteUrl
			+ '/_api/web/lists/getbytitle(\''
			+ config.listTitle + '\')/fields/getbyinternalnameortitle(\''+ config.fieldTitle + '\')';
		var payload = {'__metadata': {'type': 'SP.Field'}, 'JSLink': config.jsLink};
	    var config = {'headers' : {'X-HTTP-Method': 'MERGE' }};
		sputils.rest.post(url, payload, config);
	});
};
var config = {
	listTitle: '<your list title>',
	fieldTitle: '<your field title>',
	jsLink: '~site/<your jslink>'
};
updateJsLinkCsom(config);
updateJsLinkRest(config);

A couple of notes, to update a field we need:

  • A POST request
  • with a header ‘X-HTTP-Method’: ‘MERGE’
  • with __metadata: { type: SP.Field } and JSLink property in the payload
  • and X-RequestDigest‘ header (but it is nicely handled by sharepoint-utilities)

Trigger SP2010 Workflows using JSOM

Today I found out how to start workflows in JSOM (JavaScript Object Model in SharePoint). Nothing special, but since it is not documented, it took me a while to find a solution. Here is the code which I want to keep as simple as possible.

What you need to start a SP2010 Workflow for a list item or a document in JSOM, you need to load SP.WorkflowServices.js and you need to create the manager and get the service, then you can trigger a workflow using the workflow name, the list guid and the guid of the list item:

var ctx = SP.ClientContext.get_current();
var workflowServicesManager =
	SP.WorkflowServices.WorkflowServicesManager.newObject(ctx,
		ctx.get_web());
var service = workflowServicesManager.getWorkflowInteropService();
service.startWorkflow(workflowName, null,
	listGuid, plainItem.guid, initiationParams);

Here is the code to trigger a workflow for multiple items:

//fire the workflows
function fire2010WorkflowForListItems(ctx, listGuid, plainItems) {
	var workflowServicesManager =
		SP.WorkflowServices.WorkflowServicesManager.newObject(ctx,
			ctx.get_web());
	var service = workflowServicesManager.getWorkflowInteropService();
	for(var i = 0; i &lt; plainItems.length; i++) {
		var plainItem = plainItems[i];
		console.log('scheduling workflow for id: ', plainItem.id);
		service.startWorkflow(options.workflowName, null,
			listGuid, plainItem.guid, options.initiationParams);
	}
	console.log('now executing...');
	ctx.executeQueryAsync(function() {
		console.info('yes, workflows completed for '
			+ items.length + ' items');
	}, function() {
		console.error('it didnt go well');
	});
}

 

The code above is inspired from this gist and sharepoint stackexchange. It is a simplified version that only works for list item workflows and SharePoint 2010 workflows.
Here is an example how you can get multiple items and batch start a workflow:

//just a couple of variables
var options = {
	workflowName: 'Behörigheter',
	listName: 'Documents',
	initiationParams: {}
};
//load list items
function startWorfklows() {
	//Start 2010 Workflow for a List Item
	var ctx = SP.ClientContext.get_current();
	var web = ctx.get_web();
	var lists = web.get_lists();
	var list = lists.getByTitle(options.listName);
	ctx.load(list);
	var items = list.getItems(new SP.CamlQuery());
	ctx.load(items);
	ctx.executeQueryAsync(function() {
		var listGuid = list.get_id() + '';
		var en = items.getEnumerator();
		var plainItems = [];
		while (en.moveNext()) {
			var it = en.get_current();
			//do not take checked out files, it won't work
			if (!it.get_item('CheckoutUser')) {
				plainItems.push({id: it.get_id(), guid: it.get_item('GUID') + '' });
			}
		}
		fire2010WorkflowForListItems(ctx, listGuid, plainItems);
	}, function() {
		alert('boom');
	});
}

//Load Worfklow Js dependency
var wfScript = 'SP.WorkflowServices.js'
SP.SOD.registerSod(wfScript,
	_spPageContextInfo.webAbsoluteUrl + '/_layouts/15/SP.WorkflowServices.js');
SP.SOD.executeFunc(wfScript, '', startWorfklows);

Why I needed it

I created a simple workflow that is triggered on Item Added and Item Updated. Unfortunately there are already thousands of items in the document library. To trigger them manually is nothing good. But a simple javascript solution did exactly what I wanted.

Create and download a file in javascript

Phew, I spent a lot of time to get this to work:

  • Create a text based file in javascript – a simple csv or txt
  • Download a file in Chrome and Internet Explorer
  • Make Excel understand Unicode characters (å ä ö and many more) in csv directly.

I found many solutions on the Internet that I used to find out a solution that works for me

A deep diving jsfiddle about unicode encodings, bom, line endings: http://jsfiddle.net/kimiliini/HM4rW (unfortunately, does not work in IE). There I learned one important thing: we need a BOM in order to make Excel understand that it is not just ASCII. The BOM (byte order mark) for utf-8 is %ef%bb%bf for utf-8. Without this bom you’ll see the right characters in a text editor (except Notepad of course), but if you open the csv file directly in Excel, you’ll see wrong letters.
Other good resources are FileSaver.js: https://github.com/eligrey/FileSaver.js and download.js: http://danml.com/download.html and a discussion on that github repository issue: https://github.com/mholt/PapaParse/issues/175

This is my solution that is tested in Chrome and IE10:

function downloadContent(options) {
	if (!options || !options.content) {
		throw 'You have at least to provide content to download';
	}
	options.filename = options.filename || 'tolle.txt';
	options.type = options.type || 'text/plain;charset=utf-8';
	options.bom = options.bom || decodeURIComponent('%ef%bb%bf');

	if (window.navigator.msSaveBlob) {
		var blob = new Blob([options.bom + options.content],
                 {type: options.type });
		window.navigator.msSaveBlob(blob, options.filename);
	}
	else {
		var link = document.createElement('a');
		var content = options.bom + options.content;
		var uriScheme = ['data:', options.type, ','].join('');
		link.href = uriScheme + content;
		link.download = options.filename;
//FF requires the link in actual DOM
                document.body.appendChild(link);
		link.click();
		document.body.removeChild(link);
	}
}

//test
var separator = ';';
downloadContent({
	type: 'text/csv;charset=utf-8',
	filename: 'tolle.csv',
	content: ['ASCII', separator,
		'Åbäcka sig', separator,
		'to się podoba: żźćąęłć',
		separator, 'Яшлӑхӑма туйаймарӑм'].join('')
});

Safari

So it works on the latest Chrome (tested with 50 on Windows, Mac and Linux), Firefox (tested with 46 on Windows), Internet Explorer (tested with 11 on Windows 7). Unfortunately in Safari on a Mac, the link is opened directly in the web browser. You have to press Cmd – S to trigger Save As manually.

Part of SharePoint Utilities

I submitted this code to sputils (SharePoint Utilities) on github.

Minimal Download Strategy. Simple

There are many correct ways (1234, 5…) of making scripts work with the Minimal Download Strategy Feature (MDS) in SharePoint 2013 and 2016. But to be honest – every time I need it, I get confused. So now it is time to find a simple solution for that.

Who is better at it than the developers of the SharePoint themselves? Look at the MDS code in the built-in Display Templates:

mds-001

Let’s keep it as simple as Item_Default.js, let’s take it as it is and create our own scripts. Here is a skeletton of and MDS-ready script:

function runMyCode() {
	var time = new Date().toISOString();
	console.log('runMyCode', time );
}
runMyCode();
if (typeof(RegisterModuleInit) == 'function') {
	var scriptUrl = '/Style Library/runMyCode.js';
  RegisterModuleInit(scriptUrl, runMyCode);
}

Which boils down to this in pseudocode:

  1. Execute your code
  2. Determine if MDS is enabled
  3. If MDS is enabled
  4.       Register your code for execution

That’s it. No more overcomplicating.

Related findings

External scripts (outside SharePoint Site Collection, from CDN) within ScriptBlock of a custom action (like AddJSLink in PnP)  work withou any MDS-adjustments. The ScriptBlock adds a script tag to the head of the html document, and it the js reference is added to every AjaxDelta load. I would need more time to find out why. The good news is: it is an argument for using more CDN solutions in SharePoint.

Old stuff

A while ago I wrote two blog posts about MDS. Now I realise they were overcomplicated and the solution required customizations of the master page and 3rd-party scripts.

  1. Make your javascript code work with MDS part 1
  2. Make your javascript code work with MDS part 2

 

 

Minimal Display Template

We want to use our own Display Templates on Non-publishing sites – our team sites. Without the Publishing Feature activated you have to create an own javascript file. Here is short and concise instructions how to install it: Display Templates on Non-publishing Sites.

As described on that blog, you can make copy of an existing Item_Default.js and adjust to your needs.

I also asked Elio Struyf and I got the same tip. I did create my starter template. Here I want to share this very minimal javascript based Display Template. The real Minimal Display Template is in the SPCSR github repository: Item_Minimal.js It has been improved by Elio Stuyf himself 🙂

(function () {
  // Config contains variables that are defined in one place
  var config = {
    propertyMappings: { 'Path':null, 'Title':['Title'] }
  };
  var templateUrl;

  var register = function () {
      if ("undefined" !== typeof (Srch) && "undefined" !== typeof (Srch.U) && typeof (Srch.U.registerRenderTemplateByName) === "function") {
              Srch.U.registerRenderTemplateByName(templateUrl, render);
          }
    };
    render = function (ctx) {
      // Display template data
      var cachePreviousTemplateData = ctx.DisplayTemplateData;
        ctx.DisplayTemplateData = {
        'TemplateUrl': templateUrl,
        'TemplateType': 'Item',
        'TargetControlType': ['SearchResults', 'Content Web Parts'],
        'ManagedPropertyMapping': config.propertyMappings
        };
        var cachePreviousItemValuesFunction = ctx.ItemValues;
        ctx.ItemValues = function(slotOrPropName) {
                return Srch.ValueInfo.getCachedCtxItemValue(ctx, slotOrPropName);
        };

      // Retrieve managed property data
      var path = $getItemValue(ctx, 'Path');
      var title = $getItemValue(ctx, 'Title');

      // HTML markup for an item
      var htmlMarkup = String.format( '<div>' +
                        '<a href="{0}" title="{1}">{1}</a>' +
                                        '</div>', path, title);

        // Caching
        ctx.ItemValues = cachePreviousItemValuesFunction;
        ctx.DisplayTemplateData = cachePreviousTemplateData;

        // Return the HTML markup
        return htmlMarkup;
    };

  // Retrieve all the loaded scripts
  var allScripts = document.getElementsByTagName("script");
  // Get the last script file (this is the current DT file)
    var scriptUrl = allScripts[allScripts.length - 1].src;
    if (scriptUrl.indexOf('/_catalogs/') > 0) {
      // Remove the query string 
      if (scriptUrl.indexOf('?') > 0) {
        scriptUrl = scriptUrl.split("?")[0];
      }
      // Insert the site collection token
      templateUrl = '~sitecollection' + scriptUrl.substr(scriptUrl.indexOf('/_catalogs/'))
    // Register the template to load
    register();
      if (typeof (RegisterModuleInit) === "function" && typeof(Srch.U.replaceUrlTokens) === "function") {
          RegisterModuleInit(Srch.U.replaceUrlTokens(templateUrl), register);
      }
  }
})();

Benefits of js approach

If you choose to directly upload and mantain a javascript file you’ll get following benefits:

  • You can use the same display templates on Non-publishing sites and they work directly in SharePoint Foundation
  • You’ll get intellisense
  • You can run static code analysis agains your javascript code and you can create unit tests
  • It is more readable understandable from developer to developer
  • It is easier to follow best practices for javascript
  • It encourages reusing javascript components for Display Templates, CSR (JSLink) Templates.
  • You like TypeScript? Well, then it is best to skip the “grey” code in html comments.

Findings

  • You cannot use ‘use strict’ in your js file.

How to create a Display Template

Rename the Item_Minimal.js file to your name, update some properties.

Upload it to _catalogs/masterpage/Display Templates/Search/<your-folder>

Use ContentType: Display Template Code

displaytemplate-002

Template Level: Item and Managed Property Mappingsdisplaytemplate-003

Create a new Result Type. Every time you update the Display Template (item properties) you have to update the Result Type.

displaytemplate-001

Moving from Minimal to Better

Of course the minimal display template is not enough. There is a whole lot of things we can do, I prepared some tips (my own “best practices”):

  1. Learn the Search Display Template Syntax: Corey Roth – Useful js for Display Templates
  2. Follow Elio Struyf’s 10 tips for working with Display Templates (except the first one – javascript is better 🙂 )
  3. Prefer JavaScript Display Templates if you are a developer and you want to  have a better control
  4. Never update the built-in Display Templates.
  5. Centralize reused parts of Display Templates – utilities, common parts, css. Use a CDN site for that.
  6. Check out the SPCSR github repo, there are many Display Templates that you can use or be inspired from.
  7. Create your own folder in _catalogs/masterpage/Display Template/Search to have all your Display Templates within a site collection.

 

SharePoint Utilities – a promising JavaScript Framework

My colleagues at Bool have developed a new JavaScript framework for SharePoint – sharepoint utilities. It started on our DevDay last year – a whole free day when we could learn new things, try out new techniques or build something that was not even requested from a customer. I was not working on sharepoint utilities, so I almost forgot it until…

I recently  re-discovered sharepoint utilities. It is on Github, it is MIT licensed and contributions are welcome. The core of sharepoint utilities (sputils) is a set of wrappers for Search, TermStore, REST that allow you be more productive as a developer.

What I find especially compelling with that it contains some other fundamental stuff that every SharePoint developer needs:

  • XHR, you only need to use sputils.rest.get or sputils.rest.post to make Ajax requests to SharePoint, no need for jQuery or sprequestexecutor.js
  • Promise, it contains a minimalist Promise framework – no need for jQuery.Deferred. All calls to SharePoint _api are wrapped in promises. So there is a better way of making calls to SharePoint.
  • DateTime, it contains some useful functions for working with dates and time.
  • Functional JavaScript. No need for underscore.js or lodash.js.

To me, XHR and Promises are two important parts that I need in every solution. So instead of linking jQuery and preparing reusable XHR functions for SharePoint, I’d recommend using sharepoint-utilities.

A tiny tool for User Custom Actions

hehyuaf

Everybody loves User Custom Actions in SharePoint. That’s the only recommended way of customizing SharePoint. You have heard about it. Unfortunately there is no convinient way of administering them. People have their console applications or powershell scripts to add, update and delete user custom actions. It works but it is hard to open up Visual Studio or PowerShell every time you will try out an idea on a test site.

To overcome this, I have created a tiny little tool, packaged as a bookmarklet for your browser. When you click on it, it will show your existing user custom actions and you can add new user custom actions.

It is an ongoing little project, available on github, contributions are welcome. What’s left is:

  • Implement Delete operation
  • Implement Update operation
  • Improve the UI.

Here is how it looks today:

usercustomaction-001

To use this tool, add a new bookmark in your browser and copy the content of bookmarklet.html as the url.

Resources on that topic:

Creating a bookmarklet for Exporting Web Parts

REST API for User Custom Actions (MSDN)

What it can be used for

It can be used on test sites to try out ideas. But do not use it in Production. On ther other hand, parts of this script can be used in other scenarios, like adding new functionality by site owners directly from the browser, where data is predefined.

How to add a JavaScript code

You can use this boilerplate code to add a UserCustomAction, just put it into the textarea and click “Add new”:


var fileref=document.createElement("script");
fileref.setAttribute.src= "YOUR-SCRIPT";
document.getElementsByTagName("head")[0].appendChild(fileref);

Best practices for User Custom Actions

  • Add a ScriptBlock instead of ScriptSrc. In that way you can reference javascript and css files outside you site collection – ultimately from a CDN site.

Bypass all custom jslink

bypasscustomjslink-001

Client Side Rendering (CSR) and jslink are great for customizing lists and forms in SharePoint. In my current project we use it a lot of it. A disadvantage of that path, although, is that it might occur javascript errors, during the development phase, but also in production. We do, of course, our best to leverage the best jslink code, but unfortunately we have to live with the fact that errors can occur, especially when we use it for NewForm, EditForm, DisplayForm and View (in list and grid).

If an error occurs, it won’t stop the rest of javascript (it is wrapped in try and catch by SharePoint), but the fields will still not function as intended. It can also be some “corrupt” or old data in the field value that will “break” the jslink code.

I would like to suggest one little fix, an idea I’ve come up to in my jslink-heavy project:

Use a custom url parameter to stop all custom jslink execution.

The query string parameter can be called bypasscustomjslink=true

In every custom jslink, start with this line of code:

That’s it. If you have this in place, you can just manually add this to your url in browser:

?bypasscustomjslink=true

or

&bypasscustomjslink=true

Then all the customized fields and views will be uncustomized until bypasscustomjslink=true is removed. While viewing and editing list items in this uncustomized mode, you can access and repair data as if you never had adjusted it with jslink.

Using this does not mean you can “relax” and start writing crappy code. You still have to produce good code and anticipate all possible errors. bypasscustomjslink is just a convenient “emergency exit” aimed for developers and support to quickly solve problems without needing to reset the JSLink property on fields and list views.

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 = &amp;quot;tolle&amp;quot;;
        var loadingImg = _spPageContextInfo.webAbsoluteUrl + &amp;quot;/_layouts/images/loadingcirclests16.gif&amp;quot;;
        //unfortunately SP.Utilities.Utility is not defined at this stage
        //SP.Utilities.Utility.getImageUrl(&amp;quot;loadingcirclests16.gif&amp;quot;);       
        return ['&amp;lt;div id=&amp;quot;', containerId, '&amp;quot;&amp;gt;&amp;lt;img src=&amp;quot;', loadingImg, '&amp;quot;/&amp;gt;&amp;lt;/div&amp;gt;'].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(&amp;quot;yippie&amp;quot;);
        var data = JSON.parse(response.body);
        var imgSrc = data.d.DragAndDropImage;
        var container = document.getElementById(&amp;quot;tolle&amp;quot;);
        container.innerHTML = ['&amp;lt;img src=&amp;quot;', imgSrc, '&amp;quot;/&amp;gt;'].join('');
    }

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

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);
})();
Вула Чăвашла

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

Discovering SharePoint

And going crazy doing it

Bram de Jager talking Office 365, SharePoint and Azure

My view and thoughts on Productivity and more.

My programming life

and everything in between

SharePoint Development Lab by @avishnyakov

It is a good place to share some SharePoint stories and development practices.

SharePoint Dragons

Nikander & Margriet on SharePoint

RealActivity - Real-time and trustworthy

Blog site of founder, RealActivty - Paul J. Swider

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

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, ya mama

Treacle tarts for great justice

... And All That JS

JavaScript, Web Apps and SharePoint