CHUVASH.eu

CHunky Universe of Vigourous Astonishing SHarepoint :)

Tag Archives: lab

Multiple instances of javascript webparts on the same page

Javascript has become popular among many SharePoint developers thanks to easy and fast jQuery, CSOM, SPServices and many other javascript libraries.  That can make solutions modern and fast. On the other hand developers should be aware of more things (some of them at Bamboo Team Blog). One of those is scoping of javascript webparts. The problem a developer has to consider: what happens if a user creates two or more instances of the same beautiful webpart on the page?

Let’s go and lab 🙂

I’ll create a solution for this lab: sp-lend-id.ikkelen. This time it will be a sandboxed solution.

This solution contains a webpart:

<%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %>
<%@ Assembly Name="Microsoft.Web.CommandUI, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 
<%@ Control Language="C#" AutoEventWireup="true" 
    CodeBehind="Ikkelen.ascx.cs" Inherits="sp_lend_id.ikkelen.Ikkelen.Ikkelen" %>
<link rel="stylesheet" href="/sp-lend-id/ikkelen.css"/>
<script type="text/javascript" 
    src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript" 
    src="/sp-lend-id/ikkelen.js"></script>
<div id="notification-area"></div>
<input type="button" id="clickMe" value="Click me to show a notification"/>

a javascript file:

function notifyIkkelen() {
    jQuery("#notification-area")
        .append(jQuery("<div class='notification'>Tada!</div>"));
}
function initIkkelen() {
    jQuery("#clickMe").on({
        click: notifyIkkelen
    });
}
jQuery(document).on({
    ready: initIkkelen
});

And a css file for notifications inpired from twitter bootstrap alerts:

.notification {
    font-size: 20px;
    padding: 8px 35px 8px 14px;
    margin-bottom: 18px;
    color: #C09853;
    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
    background-color: #FCF8E3;
    border: 1px solid #FBEED5;
    border-radius: 4px;
}

Well let’s add to a page:

It works just fine. Now it’s time to add another one:

Well, this doesn’t work as it is excepted. When you click on the first button, it creates two notifications, when you click on the second button (which has the same id), nothing happens.

What shall we do about it?

Proposition 1: Make advantage of ASP.NET built-in id genereation

If you want to use your ids and make sure they don’t conflict, just add runat=”server” to your divs and in jQuery selector use ClientID:

That generates unique ids. But unfortunately it isn’t enough:

Now every time I click on the first button or the second button a notification is added to the second notification area (with the last loaded auto-generated id).

Allright, what if we “scope” it by using an anonymous function as a click event listener which won’t be overwritten:

function initIkkelen() {
    jQuery("#<%= clickMe.ClientID %>").on({
        click: function() {
            jQuery("#<%= notificationArea.ClientID %>")
                .append(jQuery("<div class='notification'>Tada!</div>"));
            }
    });
}
jQuery(document).on({
    ready: initIkkelen
});

And it works:

Okay, it works, but it sucks. You must use anonymous functions, you must put your javascript directly into markup, you must user runat=server which will generate other junk along your ids. It must be another way. We start over. Put the javascript code back to ikkelen.js, remove runat=server and ClientID.

Proposition 2. Use webpart scopes

First of all we have to remove all ids, because ids cannot be used more than once on a page. We can use just class attributes as in many projects. But there can be problems as well, because class attributes are a designer’s domain. Your solution should not break if you as designer then add and remove classes to adjust the look. Let’s take inspiration from jQuery Mobile and data-role attributes.
Ikkelen.ascx:

<div data-role="notification-area"></div>
<input type="button" data-role="clickMe" value="Click me to show a notification"/>

ikkelen.js:

function notifyIkkelen() {
    jQuery("[data-role='notification-area']")
        .append(jQuery("<div class='notification'>Tada!</div>"));
}

function initIkkelen() {
    jQuery("[data-role='clickMe']").on({
        click: notifyIkkelen
    });
}

$(document).on({
    ready: initIkkelen
});

Well, id problem is solved, but the events are fired four times:

Here when I click every button, four notification are added.

Now we must find out how we can limit our javascript and especially jQuery selectors to only one webpart. First of all we close our javascript code to a module:

(function($) {
    function notifyIkkelen() {
        $("[data-role='notification-area']")
            .append($("<div class='notification'>Tada!</div>"));
    }

    function initIkkelen() {
        $("[data-role='clickMe']").on({
            click: notifyIkkelen
        });
    }

    $(document).on({
        ready: initIkkelen
    });
})(jQuery);

The next question is what we can use to define a webpart scope. Let’s try to get current webpart id by using this.ClientID and put the id into our module, so it’s time to rewrite the module:

function ikkelen($, webpartId) {
    var webpart;
    function notifyIkkelen() {
        webpart.find("[data-role='notification-area']")
            .append($("<div class='notification'>Tada!</div>"));
    }

    function initIkkelen() {
        webpart = $("#" + webpartId);
        webpart.find("[data-role='clickMe']").on({
            click: notifyIkkelen
        });
    }

    $(document).on({
        ready: initIkkelen
    });
}

In this module which will be invoked in every instance of our webpart we create a jQuery reference to webpart (document ready). and traverse html elements only inside a webpart, by using jQuery find function.

So when we put the current webpart id into our ikkelen function along with jQuery reference:

<script type="text/javascript">
    ikkelen(jQuery, "<%= this.ClientID %>");
</script>

It works as expected:

Now a new notification is added in the webpart in which I click on the button.

Do you have other ideas about this topic? Let me know!

As I said, check out the code in my github repository:  sp-lend-id.ikkelen.

UPDATE.

If the webpart is  the classical visual webpart (farm solution, with a user control), use Parent instead of this:

(jQuery, "<%= this.Parent.ClientID %>")
Advertisements

Web Application Properties as JSON

I saw an interesting question on sharepoint.stackexchange: How to access a Web application/Farm level property bag via jQuery/Javascript/ClientContext. Some time ago I tested a custom http handler, so I wanted to try a custom httphandler for this as well. It worked. Here more details:

Just deploy sp-lend-id.tupsam from the solution. If you don’t have any properties in your web application, just add some:

asnp microsoft.sharepoint.powershell
$app = get-spwebapplication http://dev
$app.Properties.Add("Santa", "Claus")
$app.Properties.Add("Ded", "Moroz")
$app.Properties.Add("Hel", "Muci")
$app.Update()

Then just to test open the httphandler directly in the browser:

http:///_layouts/sp-lend-id/tupsam/properties.ashx

Then add the script to your solution where you need it (customaction, masterpage…) and whenever you want some web application property just use it:

_webAppProperties.Santa

nodeunit and SharePoint: unit tests in javascript

nodeunit is a (relatively) new test framework for javascript, mainly for node, but it can be run in a browser as well. The most popular framework for testing javascript is Qunit, but I’ll lab with it another time. I found nodeunit tests in moment.js – the best date handling framework for javascript and it worked very well.

So first of all, why should we test? The best answer is actually: Life is to short for manual testing (it was actually the slogan at the Google London Test Automation Conference 2007.

Why nodeunit?

Nodeunit can be run directly on server, it can test your javascript models. Even SharePoint related stuff can be tested outside the web browser. You can even automate your javascript unit tests before you build or deploy.

Nodeunit basics

We create a very simple javascript file in a sharepoint solution: CustomerModel:

Note the last if statement. This just defines what to export in nodejs.

To be able to run nodeunit tests on server we have to install node.js. Then we have to install nodeunit with npm globally:

npm install nodeunit -g

Then create a test file: TestCustomerModel.js:

Btw, here is my solution:

Next, run the test:

nodeunit TestCustomerModel.js

Allright, something is wrong:

Aha, we forgot the space. Let’s fix it:

And the test result is much better 😀

Nodeunit in web browser

nodeunit can be run in a web browser as well. I’ll test the same CustomerModel in this lab. Very simple stuff so far 😀

We create an application page, called Test.aspx. Download “nodeunit.js“. If you load javascript files in your master page and the tests are dependent of them, make sure you have the same master page in your application page:

Create browser/TestCustomerModel.js:

Change the Test.aspx:

When you deploy this as an aspx page, you’ll not see the test results, because nodeunit writes directly to body element, which is not rendered… We have to change nodeunit.js a little bit..

Then we can see the result on the page:

The whole solution for this lab can be found in my repo on github.

Enable Save in IE9 mode

Wouldn’t it be nice to use html5 and css3 in SharePoint? No problems, there is actually v5 master out there, created by Kyle Schaefer. Or just use h5ml5 and css3 right away in your webparts and pages. But there is a big problem. It doesn’t work in IE9–. One of the issues (even listed by Kyle) is that “save” doesn’t work in modal dialogs where Rich Text Editor is used. Especialy it is for modal dialogs.

The problem is not IE9, neither html5 and css3. After some digging in javascript code which is shipped with SharePoint I found out that the problem is some legacy javascript code which is not supported by IE9 but in IE8– (and compatibility mode).

When we set IE9 mode in IE9 Dev Tools (F12) and go to Tasks list and try to create a task, we’ll get an error:

It is RTE_GetEditorIFrame from init.js:

function RTE_GetEditorIFrame(strBaseElementID)
{ULSopi:;
	var ifmEditor=null;
	var doc=document;
	if ((null !=doc.frames) && (doc.frames.length==0) && (doc.parentWindow.parent !=null))
	{
		doc=doc.parentWindow.parent.document;
	}
	if ((null !=doc.frames) && (doc.frames.length > 0))
	{
		var ifmContainer=doc.getElementById(RTE_GetEditorIFrameID(strBaseElementID));
		if (ifmContainer !=null)
		{
			ifmEditor=doc.frames(RTE_GetEditorIFrameID(strBaseElementID));
		}
	}
	return ifmEditor;
}

So it is document.frames(“…”), not the usual document.frames[“…”]. What is that VB-style about?

Okay, the reason why this error isn’t raised in other browsers is just because this weird code isn’t run (Try to set break point there in Chrome or Firefox). And that’s why you never see Rich Text Editor toolbar in EditForm.aspx in Chrome and Firefox. To prove this, try to remove the Rich Text Column in your list. Then you can use “Save” button again, with IE9 mode!

Now we know the reason. How can we solve it. To do so we must find out where javascript defines whether to run or not Rich Text Editor toolbar.

First RTE_GetEditorIframe is invoked by RTE_ConvertTextAreaToRichEdit which is invoked by an inline script directly from NewForm.aspx:

if (browseris.ie5up 
  && (browseris.win32 || browseris.win64bit) 
    && !IsAccessibilityFeatureEnabled()){
    RTE_ConvertTextAreaToRichEdit("ctl00_m_g_0a9abd43_19a2_461e_b46f_6a1ae22a7ff1_ctl00_ctl05_ctl06_ctl00_ctl00_ctl04_ctl00_ctl00_TextField"
      , true, false, "", "1044", null, true, null, null, null
        ,"Compatible", "\u002ft001",null,null,null,null);
    }
else{
  document.write(" <br /><span class=\"ms-formdescription\"><a href='javascript:HelpWindowKey(\"nsrichtext\")'>Klikk her for hjelp med å legge til grunnleggende HTML-formatering.</a></span> <br />");
};

So there it is where the fishy code is invoked. Okay how can we intervene here to enable Save button? All solutions are based on disabling the bad code which is used to “convert” textarea to RichEdit.

1. Solution number one override RTE_ConvertTextAreaToRichEdit, just in style:

function RTE_ConvertTextAreaToRichEdit() {};

2. Solution number two: override IsAccessibilityFeatureEnabled, or this one:

function SetIsAccessibilityFeatureEnabled(f)
{ULSA13:;
	if (f)
		document.cookie="WSS_AccessibilityFeature=true;path=/;";
	else
		document.cookie="WSS_AccessibilityFeature=false;path=/;";
	var hiddenAnchor=document.getElementById("HiddenAnchor");
	var event;
	if (browseris.ie)
		event={ "srcElement" : hiddenAnchor , "fakeEvent" : 1, "enableStatus" : f};
	else
		event={ "target" : hiddenAnchor , "fakeEvent" : 1, "enableStatus" : f};
	if (hiddenAnchor==null || hiddenAnchor.onclick==null)
		return;
	hiddenAnchor.onclick(event);	
}

So to fool Accessibility, just set your own cookie: document.cookie=”WSS_AccessibilityFeature=true;path=/;”. But: don’t do that. It is really bad idea. Many features will stop working.

3. Solution number three is to override browseris. browseris is a browser which holds the information about the browser, simple. Here is how it is defined in init.js:

function Browseris () {ULSA13:;
	var agt=navigator.userAgent.toLowerCase();
	this.osver=1.0;
	if (agt)
	{
		var stOSVer=agt.substring(agt.indexOf("windows ")+11);
		this.osver=parseFloat(stOSVer);
	}
	this.major=parseInt(navigator.appVersion);
	this.nav=((agt.indexOf('mozilla')!=-1)&&((agt.indexOf('spoofer')==-1) && (agt.indexOf('compatible')==-1)));
	this.nav6=this.nav && (this.major==5);
	this.nav6up=this.nav && (this.major >=5);
	this.nav7up=false;
	if (this.nav6up)
	{
		var navIdx=agt.indexOf("netscape/");
		if (navIdx >=0 )
			this.nav7up=parseInt(agt.substring(navIdx+9)) >=7;
	}
	this.ie=(agt.indexOf("msie")!=-1);
	this.aol=this.ie && agt.indexOf(" aol ")!=-1;
	if (this.ie)
		{
		var stIEVer=agt.substring(agt.indexOf("msie ")+5);
		this.iever=parseInt(stIEVer);
		this.verIEFull=parseFloat(stIEVer);
		}
	else
		this.iever=0;
	this.ie4up=this.ie && (this.major >=4);
	this.ie5up=this.ie && (this.iever >=5);
	this.ie55up=this.ie && (this.verIEFull >=5.5);
	this.ie6up=this.ie && (this.iever >=6);
	this.ie7down=this.ie && (this.iever =7);
	this.ie8standard=this.ie && document.documentMode && (document.documentMode==8);
	this.winnt=((agt.indexOf("winnt")!=-1)||(agt.indexOf("windows nt")!=-1));
	this.win32=((this.major >=4) && (navigator.platform=="Win32")) ||
		(agt.indexOf("win32")!=-1) || (agt.indexOf("32bit")!=-1);
	this.win64bit=(agt.indexOf("win64")!=-1);
		this.win=this.winnt || this.win32 || this.win64bit;
	this.mac=(agt.indexOf("mac")!=-1);
	this.w3c=this.nav6up;
	this.safari=(agt.indexOf("webkit")!=-1);
	this.safari125up=false;
	this.safari3up=false;
	if (this.safari && this.major >=5)
	{
		var navIdx=agt.indexOf("webkit/");
		if (navIdx >=0)
			this.safari125up=parseInt(agt.substring(navIdx+7)) >=125;
		var verIdx=agt.indexOf("version/");
		if (verIdx >=0)
	            this.safari3up=parseInt(agt.substring(verIdx+8)) >=3;
	}
	this.firefox=this.nav && (agt.indexOf("firefox") !=-1);
	this.firefox3up=false;
	this.firefox36up=false;
	if (this.firefox && this.major >=5)
	{
	    var ffVerIdx=agt.indexOf("firefox/");
	    if (ffVerIdx >=0)
	    {
		var firefoxVStr=agt.substring(ffVerIdx+8);
	        this.firefox3up=parseInt(firefoxVStr) >=3;
		this.firefox36up=parseFloat(firefoxVStr) >=3.6;
	    }
	}
}
var browseris=new Browseris();
var bis=browseris;

So to fool this, just set:

 var browseris = {};

But you have to do it after init.js, so wrap into ExecuteOrDelayUntilScriptLoaded:

ExecuteOrDelayUntilScriptLoaded(function() { 
  var browseris = {}; 
}, "init.js");

After that you can press Save button. You’ll lose the Rich Text Editor, but you’ll have the nice features from CSS3.

DISCLAIMER: It is just an exploration lab. There is no warranty that it will work across the whole site. Don’t ever do such things things at home!

css3 fixers

If you just want to use css3 in your design and still use team functionality, take a look on pie.htc, jQuery.corner or iecss3.

 

Update 2015-09-16 Other Related Issues

Recently me and my colleague had to fix this issue again in SharePoint 2010. What we found was failing select elements (Choice field or Lookup field) in forms. There are some js functions that do not run in other web browsers rather than in IE in IE9 mode. All fail because of the inability of getting custom html element attributes.


//This is an override of FilterChoice (in core.js)
//We override this line: var strOpts=ctrl.choices; -> var strOpts=ctrl.getAttribute("choices");
//The reason is that this does not work in IE9 mode
//Don't forget this workaround. It might lead to other problems
function FilterChoice_new(opt, ctrl, strVal, filterVal) {
   var i;
   var cOpt=0;
   var bSelected=false;
   var strHtml="";
   var strId=opt.id;
   var strName=opt.name;
   var strMatch="";
   var strMatchVal="";
   var strOpts=ctrl.getAttribute("choices"); //THIS ONE
   var rgopt=strOpts.split("|");
   var x=AbsLeft(ctrl);
   var y=AbsTop(ctrl)+ctrl.offsetHeight;
   var elmWorkspace=document.getElementById("s4-workspace");
   if(elmWorkspace)
                                y -=AbsTop(elmWorkspace);
   var strHidden=ctrl.optHid;
   var iMac=rgopt.length - 1;
   var iMatch=-1;
   var unlimitedLength=false;
   var strSelectedLower="";
   if (opt !=null && opt.selectedIndex >=0)
   {
      bSelected=true;
      strSelectedLower=opt.options[opt.selectedIndex].innerText;
   }
   for (i=0; i < rgopt.length; i=i+2)
   {
      var strOpt=rgopt[i];
      while (i < iMac - 1 && rgopt[i+1].length==0)
      {
         strOpt=strOpt+"|";
         i++;
         if (i < iMac - 1)
         {
                                      strOpt=strOpt+rgopt[i+1];
         }
         i++;
      }
      var strValue=rgopt[i+1];
      var strLowerOpt=strOpt.toLocaleLowerCase();
      var strLowerVal=strVal.toLocaleLowerCase();
      if (filterVal.length !=0)
                                   bSelected=true;
      if (strLowerOpt.indexOf(strLowerVal)==0)
      {
         var strLowerFilterVal=filterVal.toLocaleLowerCase();
         if ((strLowerFilterVal.length !=0) && (strLowerOpt.indexOf(strLowerFilterVal)==0) && (strMatch.length==0))
                                      bSelected=false;
         if (strLowerOpt.length > 20)
         {
                                      unlimitedLength=true;
         }
         if (!bSelected || strLowerOpt==strSelectedLower)
         {
            strHtml+="<option selected value=\""+strValue+"\">"+STSHtmlEncode(strOpt)+"</option>";
            bSelected=true;
            strMatch=strOpt;
            strMatchVal=strValue;
            iMatch=i;
         }
         else
         {
            strHtml+="<option value=\""+strValue+"\">"+STSHtmlEncode(strOpt)+"</option>";
         }
         cOpt++;
      }
   }
   var strHandler=" ondblclick=\"HandleOptDblClick()\" onkeydown=\"HandleOptKeyDown()\"";
   var strOptHtml="";
   if (unlimitedLength)
   {
      strOptHtml="<select tabIndex=\"-1\" ctrl=\""+ctrl.id+"\" name=\""+strName+"\" id=\""+strId+"\""+strHandler;
   }
   else
   {
      strOptHtml="<select class=\"ms-lookuptypeindropdown\" tabIndex=\"-1\" ctrl=\""+ctrl.id+"\" name=\""+strName+"\" id=\""+strId+"\""+strHandler;
   }
   if (cOpt==0)
   {
      strOptHtml+=" style=\"display:none;position:absolute;z-index:2;left:" + x 
        + "px;top:" + y + "px\" onfocusout=\"OptLoseFocus(this)\"></select>";
   }
   else
   {
      strOptHtml+=" style=\"position:absolute;z-index:2;left:"+x+"px;top:"+y+" size=\""
        +(cOpt <=8 ? cOpt : 8)+"\""+ (cOpt==1 ? "multiple=\"true\"" : "")
        + " onfocusout=\"OptLoseFocus(this)\">"+  strHtml + "</select>";
   }
   opt.outerHTML=strOptHtml;
   var hid=document.getElementById(strHidden);
   if (iMatch !=0 || rgopt[1] !="0" )
      hid.value=strMatchVal;
   else
      hid.value="0";
   if (iMatch !=0 || rgopt[1] !="0" )
      return strMatch;
   else return "";
}
function SetCtrlMatch_new(ctrl, opt) {
  //ctrl.getAttribute("optHid") instead of ctrl.optHid
  var hid=document.getElementById(ctrl.getAttribute("optHid"));
  hid.value=opt.options[opt.selectedIndex].value;
  if (hid.value !=0)
    ctrl.match=opt.options[opt.selectedIndex].innerText;
  else
    ctrl.match="";
}

function HandleOptDblClick_new() {
  var opt=event.srcElement;
  //opt.getAttribute("ctrl") instead of opt.ctrl
  var ctrl=document.getElementById(opt.getAttribute("ctrl"));
  _SetCtrlFromOpt(ctrl, opt);
  SetCtrlMatch(ctrl, opt);
  opt.style.display="none";
}

function _SetCtrlFromOpt_new(ctrl, opt) {
  var hidId = ctrl.getAttribute("optHid");
  if (!hidId) { return; }
  //ctrl.getAttribute("optHid") instead of ctrl.optHid
  var hid=document.getElementById(ctrl.getAttribute("optHid"));
  hid.value=opt.options[opt.selectedIndex].value;
  if (opt.options[opt.selectedIndex].value==0)
  {
    ctrl.value=opt.options[opt.selectedIndex].innerText;
    ctrl.match="";
  }
  else
  {
    ctrl.value=opt.options[opt.selectedIndex].innerText;
    ctrl.match=ctrl.value;
  }
}

function HandleOptKeyDown_new() {
  var opt=event.srcElement;
  //opt.getAttribute("ctrl") instead of opt.ctrl
  var ctrl=document.getElementById(opt.getAttribute("ctrl"));
  var key=event.keyCode;
  switch (key)
  {
  case 13:
  case 9:
    _SetCtrlFromOpt(ctrl, opt)
    event.returnValue=false;
    opt.style.display="none";
    return;
  default:
    break;
  }
  return;
}

ExecuteOrDelayUntilScriptLoaded(function() {
  FilterChoice = FilterChoice_new;
  SetCtrlMatch = SetCtrlMatch_new;
  HandleOptDblClick = HandleOptDblClick_new;
  _SetCtrlFromOpt = _SetCtrlFromOpt_new;
  HandleOptKeyDown = HandleOptKeyDown_new;
}, "core.js");


 

Вула Чăвашла

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 Microsoft healthcare services and solution advisory firm.

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