Monday, July 28, 2008

Customized Input Forms: The JavaScript Approach

Correction [02.13.2009]: I have recently been informed of an issue with field values not saving when they are disabled for the user. Please see my post here for updated code.

If any of you have had to customize the input forms for lists or libraries in SharePoint, you'll know that it is not easy. You can customize the form with a custom list form web part, but you better not delete the original list form web part, otherwise your list gets hosed. You can have both on the page (with the original web part hidden), but then you lose attachment support with JavaScript errors. If you create a new page without the original list form web part, then you get a nice JavaScript alert saying that the form has been customized to not allow attachments. If you used the custom list form web part, then you might find it cumbersome and tedious to make changes when new columns are added. I have tried countless recommendations for modifying the list input form, and none of them really seemed to work cleanly and effectively. Until I stumbled across the forum discussion located here on MSDN Forums.

Of all of the ideas proposed in this forum topic, I found tscheifler's usage of JavaScript quite intriguing. There were some limitations that I found when implementing their exact solution (most notably around disabling columns of type: Lookup, Date and Time, and Multiple lines of Text). Extending this, and incorporating various other bits of information lying about the web, I ended up with a solution I am quite happy with. This assumes you have an existing list with several fields you would like to hide/disable for certain user groups. We will begin by modifying the "NewForm.aspx" page, but these steps will work for any of the input forms:
  1. Open the "New Form" for the list and remove all query string variables (should end up with http://server/site/listname/NewForm.aspx)
  2. Append "?ToolPaneView=2" onto the end of the url (http://server/site/listname/NewForm.aspx?ToolPaneView=2)

    Note: this places the page into "Add a Web Part" view
  3. Add a new "Content Editor Web Part"(CEWP) to the page after the existing web part
  4. Modify the source view of the CEWP to include this code:
    <SCRIPT LANGUAGE="JavaScript">
    <!--
    function disableChildren(currentElement)
    {
    if (currentElement)
    {
    if(currentElement.tagName == "IFRAME")
    {
    frm = window.frames[currentElement.id].document;
    disableChildren(frm.getElementsByTagName("html")[0]);
    }
    var i=0;
    var currentElementChild=currentElement.childNodes[i];
    while (currentElementChild)
    {
    disableChildren(currentElementChild);
    i++;
    currentElementChild=currentElement.childNodes[i];
    }
    if (currentElement.tagName)
    {
    currentElement.setAttribute("disabled", "true");
    currentElement.setAttribute("contentEditable",
    "false");
    currentElement.setAttribute("onclick", "");
    currentElement.removeAttribute("href");
    }
    }
    }
    
    function hideRowsAfter(currentRow)
    {
    row = currentRow.nextSibling
    while (row)
    {
    row.style.display = "none";
    row = row.nextSibling;
    }
    }
    
    function findControl(FieldName, opp)
    {
    FieldName = "FieldName=\"" + FieldName + "\"";
    //get all comments
    var arr = document.getElementsByTagName("!");
    for (var i=0;i < arr.length; i++ ) 
    {
    // now match the field name
    if (arr[i].innerHTML.indexOf(FieldName) >= 0)
    {
    switch(opp) 
    {
    case 0:  //disable all children
    disableChildren(arr[i].parentNode.parentNode);
    break;
    case 1:  //hide row
    arr[i].parentNode.parentNode.style.display="none";
    break;
    case 2:  //hide all rows after current
    hideRowsAfter(arr[i].parentNode.parentNode);
    break;
    default:
    break;
    }
    }
    }
    }
    
    function disableControls(inputArray)
    {
    for (var i=0; i < inputArray.length; i++)
    {
    findControl(inputArray[i], 0);
    }
    }
    
    function hideControls(inputArray)
    {
    for (var i=0; i < inputArray.length; i++)
    {
    findControl(inputArray[i], 1);
    }
    }
    
    function hideControlsAfter(input)
    {
    findControl(input, 2);
    }
    
    //Usage:
    // disableControls(["Field Name 1", "Field Name 2"..]);
    // hideControls(["Field Name 1", "Field Name 2"..]);
    // hideControlsAfter("Field Name");
    
    //-->
    </SCRIPT>
  5. Follow the "Usage notes" to add calls to the disableControls, hideControls, or hideControlsAfter functions which will modify the fields displayed on the page

    Note: you can find the values to put in place of "Field Name 1" and "Field Name 2" by viewing the source view for the page and searching for: FieldName="
    The value within the double quotation marks will be what you enter inside the arrays in the hide/disable function calls.
  6. In the CEWP's tool pane, add security groups or audiences which should have the fields hidden/disabled to the "Target Audience" setting
  7. Save the web part properties, and click the "Exit Edit Mode" link in the upper right below "Site Actions".
Now you should see all of the fields hidden or disabled based on your security settings specified within the CEWP.

To extend this into a more reusable solution, I created a custom CEWP with this JavaScript code, exported it to a .dwp file, and then uploaded it to the Web Part gallery. Then, the only modifications needed are to add this web part to the page (under ToolPaneView=2 view), add the "usage" functions, and add the security groups to the targeted audiences.

This solution now hides/disables fields specified within the original list input form, which retains the attachment functionality, while being able to easily make modifications when new fields are added/modified based on column ordering.

However, there are some limitations to this method. None of which were show stoppers for me, since this method is very easy to undo (simply remove the CEWP from the forms). Especially in the interim until Microsoft is able to fix this little problem and offer a clean, efficient, and effective method of customizing input forms. These limitations are:
  • Security groups or audiences need to be created for the group of users which should have limited access to list fields, which excludes the users with access to the restricted fields.
  • You cannot make any of the hidden/disabled fields required unless you specify default values, as the fields will still remain on the page and submit data, they are just hidden to the user
  • If the user employs firebug or other plugins which can alter JavaScript, they could unhide/undisable the fields you removed and then manipulate the data
  • Information in these fields is still visible to users if the columns are displayed within list views, as well as Source View of the input form

Enjoy!
--andrew

4 comments:

  1. To hide a field and it decendants insert:

    currentElement.style.visibility='hidden';
    after
    currentElement.removeAttribute("href");

    Regarding:
    Information in these fields is still visible to users if the columns are displayed within list views, as well as Source View of the input form

    ReplyDelete
  2. Anonymous,
    The function disableChildren is intended to leave the field visible but not editable.

    I handle hiding fields in the function findControl using the display attribute instead, so the hidden controls do not occupy space on the page.

    Since all of this is done with JavaScript on the input forms, unfortunately, there is no way to eliminate fields completely from the Source View of the page or from the list view.

    ReplyDelete
  3. i have customized the dispform.aspx using sharepoint designer.
    on right click of the webpart-->webpart properties-->target audience,i gave a sharepoint group name to whome its should visible.
    but this is not working?

    thnks in advance.

    ReplyDelete
  4. Akahaya,
    Something to remember here, is that the group used for the target audience should contain the users who are supposed to have restricted editing permissions. It should not contain the users who are supposed to have access to the restricted fields. (I know, it's backwards from how audiences are generally used.)

    Make sure you are targeting the new Content Editor Web Part for the audience, and not the ListFormWebPart (which comes on the page by default).

    You might also try using the same group to target web parts on a different page to see if your audience/group is functioning properly.

    ReplyDelete