Why are my publishing pages saying I have so many spelling errors?


In my publishing site, I have a basic custom content type that is based on the SharePoint out the box Page Content type. I have created a custom page layout. My layout contains a PublishingRollupImage, PublishingPageContent, and I’ve created a section on my page layout that only shows when you edit the page which contains PublishingContact. The publishing contact is mainly for search.

When my page is in edit mode, the screen looks like the following:

So I add a picture and some content to the page, and then go to publish it. The Page Content has been double checked and there is no spelling mistakes in there. However when I click publish, I get a pop-up box saying there are 73 spelling error(s) found. How is this possible? There isn’t even 73 words on the page.

The clue why this is happening is actually shown on the page. Where I have Page Owner at the bottom of the page, it clearly states “Spelling Errors Found.” But why? It’s just my name. If you click on the red text, it brings up a pop up, showing you what the spelling mistakes are.

What is actually happening is the user control HTML is being read. The user control has <textarea> which is hidden by css. This textarea is used by SharePoint to correctly display the User on the page, with the link to the user profile information. It is this text that is being spell checked.

There is a simple fix that you can add to your page layouts (assuming your page has jQuery already loaded). The textarea needs an attribute added to it called “excludeFromSpellCheck” which makes the SharePoint spell checker ignore the text within it. On your page layout page, find the ContentPlaceHolderID=”PlaceHolderMain” and right at the bottom of this section add a PublishingWebControls:EditModePanel with the following Javascript inside.

<PublishingWebControls:EditModePanel ID="SpellCheckEditModePanel" runat="server">
<script type="text/javascript">
jQuery(".ms-usereditor textarea").attr("excludeFromSpellCheck", "true");
</script>
</PublishingWebControls:EditModePanel>

The reason why we put it inside an EditModePanel is because the spell checking is only carried out when the page is in edit mode, so there is no need to run the JavaScript in display mode.

As you can see after implementing the jQuery change, we no longer get any spelling errors found.

Advertisements

Make SPLongOperation postback to same page. (Well a workaround at least)


The title here “Make SPLongOperation postback to the same page. (Well a workaround at least)” is because there is no actual way of making the SPLongOperation postback to the same page. However I have been able to achieve a similar affect. First let’s explain what the SPLongOperation is.

The SPLongOperration class in SharePoint is a brilliant way to inform your users that a long operation is running in the background, without freezing or locking up the UI while the process is running. You may have seen the screen many times when creating a site.

The code to create this is very simple.


 using (SPLongOperation longOperation = new SPLongOperation(this.Page))

{

 //Show Text for the Top Line on the screen.

  longOperation.LeadingHTML = "Provisioning Sites";

 //Show text for the Bottom line on the screen.

  longOperation.TrailingHTML = "Please wait while the sites are being provisioned.";

  longOperation.Begin();

  try

 {

  //The code that will take it's time.

  Thread.Sleep(5000);

 //On complete navigate to your success/complete page.

  longOperation.End("MySuccessPage.aspx");

 }

 catch(ThreadAbortException)

 {

  //Don't do anything, this error can occur because the SPLongOperation.End

 // performs a Response.Redirect internally and doesn't take into account that other code might still be executed.

 }

 catch(Exception ex)

 {

  //When an exception happens, the page is redirected to the error page.

  //Here you can redirect to another custom page.

  SPUtitility.TransferToErroPage(ex.ToString());

 }

}

As you can see from above it is pretty simple. However the longOperation.End(“MySuccessPage.aspx”) redirects the user to another page. You can point it to the same page you are currently on such as longOperation.End(Request.Url.ToString()); but this doesn’t cause a post back.

SPLongOperation.End has 3 overloads.

  • SPLongOperation.End(string strRedirectPage)
  • SPLongOperation.End(string strProposedRedirect, SPRedirectFlags rgfRedirect, HttpContext context, string queryString)
  • SPLongOperation.End(string strProposedRedirect, SPRedirectFlags rgfRedirect, HttpContext context, string queryString, string strScript)

It is the third overload here that can help us achieve a postback like state.

The strScript is javascript that is run before the page gets redirected. If we reflect on the 3rd method we can see how this works.

public void End(string strProposedRedirect, SPRedirectFlags rgfRedirect, HttpContext context, string queryString, string strScript)

{

    string str;

    if (!SPUtility.DetermineRedirectUrl(strProposedRedirect, rgfRedirect, context, queryString, out str))

    {

        str = strProposedRedirect;

    }

    string str2 = SPHttpUtility.EcmaScriptStringLiteralEncode(str);

    string str3 = string.Format(CultureInfo.InvariantCulture, "window.location.replace(\"{0}\");", new object[] { str2 });

    if (strScript != null)

    {

        str3 = strScript + str3;

    }

    this.EndScript(str3);

}

In this method call, the javascript is added to the front of the windows.location.replace, so that your html/javascript is run first, then it redirects you to your strProposedRedirect URL. From this knowledge, if I put return false; as my strScript, it prevents any JavaScript running after and prevents the redirection of the page. However I’m still looking at the loading page.

So how does any of this help me? What am I trying to achieve?

My end goal is to perform a long process that gathers some results and then displays them on the same page once the long process has finished. Example screen shots below.

The above can be achieved by using Javascript to call a function that will post the user to the page with the required results within the post data. In my original example of how to use SPLongOperation you would replace the line Thread.Sleep(5000); with your method that would process the word file and return in a string the results. Then you need to create the javascript post form data using the results.


…

 try

 {

  //The code that will take it's time.

  WordProcessing wordprocess = new WordProcessing(FileUpload1.FileContent, FileUpload1.FileName);

//results is a global string variable.

 results = wordprocess.Process();

 //On complete navigate to your success/complete page.

  longOperation.End(Request.Url.ToString(), SPRedirectFlages.UseSource, HttpContext.Current, "", postBackToPage();

 }

 …

 

Now we need to create the JavaScript function post_Back_To_Page() which will be called.


private string postBackToPage()

        {

            StringBuilder str = new StringBuilder();

            str.Append("function post_Back_To_Page(){");

            str.Append("var method = 'post';");

            str.Append("var form = document.createElement('form');");

            str.Append("form.setAttribute('method', method);");

            str.Append("form.setAttribute('action', '" + Request.Url.ToString() + "');");

            str.Append("var data = document.createElement('input');");

            str.Append("data.setAttribute('type', 'hidden');");

            str.Append("data.setAttribute('name', 'results');");

            str.Append("data.setAttribute('value', '" + SPEncode.UrlEncode(results) + "');");

            str.Append("form.appendChild(data);");

            str.Append("document.body.appendChild(form);");

            str.Append("form.submit();");

            str.Append("};post_Back_To_Page();return false;");

            return str.ToString();

        }

Now on loading the page if there is a form data called results, we will want to grab and display that. So on your page load, add the following.


protected void Page_Load(object sender, EventArgs e)

{

  if (Request.Form["results"] != null)

 {

   txt.Results.Text = SPEncode.UrlDecodeAsUrl(Request.Form["results"]);

 }

}

At this stage, I was excited that a workaround was possible. However first time I ran this code I encountered the error “The security validation for this page is invalid.”

This is happening because I’m trying to insert data into a page. SharePoint thinks I’m a hacker, and therefore rejects the post. After extensive searching on the web, peoples suggestion was to disable the SPWebapplication.FormDigestSettings. Do not do this! It is a huge security risk to your farm. What SharePoint is after is the form digest control value.

According to MSDN http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.webcontrols.formdigest.aspx

“The FormDigest value is a generated security validation, or message digest, to help prevent the type of attack whereby a user is tricked into posting data to the server without knowing it. The security validation is specific to a user, site, and time period and expires after a configurable amount of time. When the user requests a page, the server returns the page with security validation inserted. When the user then submits the form, the server verifies that the security validation has not changed.”

We need to create a form digest value and add it to the form being posted in the Javascript. Replace the code in the method postBackToPage()


private string postBackToPage()

        {

          //Create the Form Digest String.

            StringBuilder formDigestString = new StringBuilder();

            StringWriter tw = new StringWriter(formDigestString);

            HtmlTextWriter textWriter = new HtmlTextWriter(tw);

            FormDigest formDigest = new FormDigest();

            formDigest.RenderControl(textWriter);

            string formDigestHtml = formDigestString.ToString();

            StringBuilder str = new StringBuilder();

            str.Append("function post_Back_To_Page(){");

            str.Append("var method = 'post';");

            str.Append("var form = document.createElement('form');");

            str.Append("form.setAttribute('method', method);");

            str.Append("form.setAttribute('action', '" + Request.Url.ToString() + "');");

           //Add formdigestHtml to the Form.

            str.Append("var digest = document.createElement('div');");
            str.Append("digest.innerHTML = '" + formDigestHtml + "';");

            str.Append("form.appendChild(digest);");

            str.Append("var data = document.createElement('input');");

            str.Append("data.setAttribute('type', 'hidden');");

            str.Append("data.setAttribute('name', 'results');");

            str.Append("data.setAttribute('value', '" + SPEncode.UrlEncode(results) + "');");

            str.Append("form.appendChild(data);");

            str.Append("document.body.appendChild(form);");

            str.Append("form.submit();");

            str.Append("};post_Back_To_Page();return false;");

            return str.ToString();

        }

As the title of this blog says “Well a workaround at least.” You aren’t actually posting back to the same page, but you can redirect back to the same page, passing back data that you might want to display. The form created in JavaScript can be extended to add extra elements for other data you wish to post back as well.

Calling Generic HTTPHandler from Client side


In my last blog post I explain what an HTTPHandler was, and how to implement it. In this post I will explain to you how to call your HTTPHandler using JavaScript. Please note this solution uses jQuery and assumes this is already being loaded in your page. Please follow my blog inserting jquery into your sharepoint site as a feature, to ensure that jQuery is available within your SharePoint site every time, if you currently don’t load jQuery.

This demo will allow a user to enter a name of a list on the site and return some properties regarding this list. By following the initial steps from the last blog post, create your ashx file and called it ListHandler.ashx.

  • First create a ListDetails.cs class. This class will hold the properties of the SPList we wish to pass back from the handler. Right Click the Project > Add> New Item… and create the class called ListDetails.cs
  • In ListDetails.cs class we will set up 4 properties. Title, DefaultViewURL, Description, ItemCount.

public class ListDetails

    {

        public String Title { get; set; }

        public String DefaultViewURL {get;set;}

        public String Description {get;set;}

        public String ItemCount {get;set;}

    }

  • Save the ListDetails.cs file.
  • Now open the ListHandler.ashx file. From following the last blog post you should have already implemented the IHttpHandler Interface. When implementing the IHttpHandler interface we need both IsReusable and ProcessRequest. Set the IsReusable as the following.

public bool IsReusable

        {

            get

            {

                return true;

            }

        }

 
  • The ProcessRequest method is going to check for query string name called listname. Then get the SPList object, retrieve the details we want in the ListDetails class, then JavaScriptSerialize our class to write back our results out as JavaScript.

  public void ProcessRequest(HttpContext context)

        {

            var listName = string.Empty;

            //Obtain the Query String

            if (context.Request.QueryString["listName"] != null)

            {

                listName = context.Request.QueryString["listName"];

            }

            string results = String.Empty;

            try

            {

                if (!string.IsNullOrEmpty(listName))

                {

                    //Get the SPList object.

                    SPList list = SPContext.Current.Web.Lists.TryGetList(listName);

                    if (list != null)

                        //Pass SPList to get details and convert to JSON string.

                        results = GetJSON(list);

                    else

                    {

                        throw new Exception("List Not Found!");

                    }

                }

            }

            catch (Exception ex)

            {

                throw ex;

            }

            context.Response.Write(results);

        }

        private string GetJSON(SPList list)

        {

            //Initialise the JavaScript initializer

            System.Web.Script.Serialization.JavaScriptSerializer serializer = new System.Web.Script.Serialization.JavaScriptSerializer();

            //Get ListDetails Class and obtain values.

            var curList = new ListDetails();

            curList.Title = list.Title;

            curList.Description = list.Description;

            curList.DefaultViewURL = list.DefaultViewUrl;

            curList.ItemCount = list.ItemCount.ToString();

            //Return Class Serialized for JavaScript.

            return serializer.Serialize(curList);

        }

If you save and deploy your project as is, and navigate to <Site>/_layouts/GenericHandler/ListHandler.ashx?listName=<listName>, replacing site and listName with your details you should see some results. For example I have passed in the list name Shared Documents as I’m using a TeamSite for the below results.

To create the front end, we are going to create a simple application page. Create a page called ListCaller.aspx.

  • Within the Content PlaceHolderMain put a Label, TextBox and Button. Below we will display the 4 values Title, Description, DefaultURL and ItemCount.

<asp:Content ID="Main" ContentPlaceHolderID="PlaceHolderMain" runat="server">

    <asp:Label ID="lblInstructions" runat="server" Text="Please enter a name of a list"></asp:Label>

    <asp:TextBox ID="txtName" runat="server"></asp:TextBox>

    <asp:Button ID="btnGetList" runat="server" Text="Get List Details" />

    <div id="listTitle"></div>

    <div id="listDescription"></div>

    <div id="listDefaultURL"></div>

    <div id="listItemCount"></div>

</asp:Content>

  • Create a JavaScript file. I have called mine ListCaller.js, this file is stored in layouts directory too.

  • Open this file. We are going to create a simple ajax call to our handler. On success it will return data from the handler, or it will throw an error. Create a function called getListDetails that is expecting the list name.

function getListDetails(listName) {

       jQuery.ajax({

        url: '/_layouts/GenericHandler/ListHandler.ashx',

        data: 'listName=' + listName,

        dataType: "json",

        contentType: "application/json; charset=utf-8",

        method: 'GET',

        success: function (data) {

            jQuery('#listTitle').html('<b>Title: </b>' + data.Title);

            jQuery('#listDescription').html('<b>Description: </b>' + data.Description);

            jQuery('#listDefaultURL').html('<b>Default URL: </b>' + data.DefaultViewURL);

            jQuery('#listItemCount').html('<b>Item Count: </b>' + data.ItemCount);

        },

        error: function (jqXHR, textStatus, errorThrown) {

            jQuery('#listTitle').text('Failed');

            jQuery('#listDescription').text('');

            jQuery('#listDefaultURL').text('');

            jQuery('#listItemCount').text('');

        }

    });

};

  • Now we need to call this JavaScript method from our aspx page. Open up your aspx page. Within the PlaceHolderAdditionalPageHead we need to add a link to the ListCaller.js file. Also here I’m going to add a JavaScript function that gets the value from within textbox and calls getListDetails().

<asp:Content ID="PageHead" ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">

<script type="text/javascript" src="/_layouts/GenericHandler/js/ListCaller.js"></script>

    <script>

        function details() {

          getListDetails(jQuery('#' + '<%=txtName.ClientID%>').val())

        };

    </script>

</asp:Content>

  • Lastly add an OnClientClick to your Button. Your <asp:Button will now look like below.

<asp:Button ID="btnGetList" runat="server" Text="Get List Details" OnClientClick="javascript:details();return false;"/>

By navigating to the application page, and type in a name of a library or list can click Get List Details it will return the details of that list.

Cross Domain and SharePoint Hosted Apps using CSOM


Continuing on from last week blog post on Cross Domain and SharePoint Hosted Apps using REST, today I’m going to show you how to do the same Cross Domain calls but by using CSOM.

If you remember the 2 different calls I made in my project were a call back to the host domain to retrieve the title, and to retrieve all the lists in the host web. Here I’m going to extend that project and include two additional buttons that are going to do exactly the same thing, but using CSOM.

Below is a screen shot where we last left off.

If you were following along, open the solution from last week, and let us continue.

  • Open the Default.aspx page. Here we will add the two buttons to the relevant sections. Under the last button with the id=”btnStandardRestGetTitle” but before the </div> lets add the button to get Title via CSOM.
<span><input type="button" id="btnCSOMGetTitle" value="Get Title via CSOM" onclick="getTitleCSOM()" /></span>
  • While we are in the Default.aspx page, let us add the button to get the lists via CSOM as well. Under the last button with the id=”btnStandardRestGetLists” but before the </div> lets add the button to get Lists via CSOM.
<span><input type="button" id="btnCSOMGetLists" value="Get Lists via CSOM" onclick="getListsCSOM()" /></span>
  • Open up your App.js file. Let us add the two click event handlers for the buttons we just placed on default.aspx
/*Button Click Get CSOM*/
function getTitleCSOM() {
    execCSOMTitleRequest();
}
/*button Click Get List CSOM*/
function getListsCSOM() { execCSOMListRequest();}
  • Lastly we are going to add the two functions that returns the title and Lists, with their corresponding Success and Fail handlers.
/*********************************CSOM*****************************************************/
//CSOM Cross Domain call to obtain HostWeb Title
function execCSOMTitleRequest() {
    var context;
    var factory;
    var appContextSite;
    var collList;
    //Get the client context of the AppWebUrl
    context = new SP.ClientContext(appwebUrl);
    //Get the ProxyWebRequestExecutorFactory
    factory = new SP.ProxyWebRequestExecutorFactory(appwebUrl);
    //Assign the factory to the client context.
    context.set_webRequestExecutorFactory(factory);
    //Get the app context of the Host Web using the client context of the Application.
    appContextSite = new SP.AppContextSite(context, hostwebUrl);
    //Get the Web
    this.web = appContextSite.get_web();
    //Load Web.
    context.load(this.web);
    context.executeQueryAsync(
        Function.createDelegate(this, successTitleHandlerCSOM),
        Function.createDelegate(this, errorTitleHandlerCSOM)
        );
    //success Title
    function successTitleHandlerCSOM(data) {
        $('#lblResultTitle').html("<b>Via CSOM the title is:</b> " + this.web.get_title());
    }
    //Error Title
    function errorTitleHandlerCSOM(data, errorCode, errorMessage) {
        $('#lblResultLists').html("Could not complete CSOM call: " + errorMessage);
    }
}
//CSOM Cross domain call to obtain HostWeb Lists
function execCSOMListRequest(){
    var context;
    var factory;
    var appContextSite;
    var collList;
    //Get the client context of the AppWebUrl
    context = new SP.ClientContext(appwebUrl);
    //Get the ProxyWebRequestExecutorFactory
    factory = new SP.ProxyWebRequestExecutorFactory(appwebUrl);
    //Assign the factory to the client context.
    context.set_webRequestExecutorFactory(factory);
    //Get the app context of the Host Web using the client context of the Application.
    appContextSite = new SP.AppContextSite(context, hostwebUrl);
    //Get the Web
    this.web = appContextSite.get_web();
    // Get the Web lists.
    collList = this.web.get_lists();
    //Load Lists.
    context.load(collList);
    context.executeQueryAsync(
        Function.createDelegate(this, successListHandlerCSOM),
        Function.createDelegate(this, errorListHandlerCSOM)
        );
    //Success Lists
    function successListHandlerCSOM() {
            var listEnumerator = collList.getEnumerator();
            $('#lblResultLists').html("<b>Via CSOM the lists are:</b><br/>");
        while (listEnumerator.moveNext()) {
            var oList = listEnumerator.get_current();
            $('#lblResultLists').append(oList.get_title() + " (" + oList.get_itemCount() + ")<br/>");
        }
    }
    //Error Lists
    function errorListHandlerCSOM(data, errorCode, errorMessage) {
        $('#lblResultLists').html("Could not complete CSOM Call: " + errorMessage);
    }
};
  • If we now run out solution, you should see all three ways of obtaining the title and lists from the host.

  • By clicking Get Title via CSOM this will return the title “Cannonfodder Development” in my case, exactly the same result as if we tried via REST.

  • By clicking Get Lists via CSOM this will return all the lists from my host Cannonfodder development, exactly the same list as if we tried via REST.

I hope these two blogs gave you a basic understanding of the different ways you can interact with the host in SharePoint 2013.

getDefaultSiteCollectionTermStore() – JavaScript runtime error: Unable to get property ‘toString’ of undefined or null reference


Today I was venturing in the world of JavaScript and SharePoint 2013 Hosted Apps. I was trying to perform a simple JavaScript that access the Managed Term Store to bring back results.

Take a look at my code below.


var context;

var termStore;

var groups;

$(document).ready(function () {
var scriptbase = _spPageContextInfo.webServerRelativeUrl + "/_layouts/15/";
  $.getScript(scriptbase + "SP.Runtime.js",   function () {
      $.getScript(scriptbase + "SP.js", function () {
        $.getScript(scriptbase + "SP.Taxonomy.js", function () {
              context = SP.ClientContext.get_current();
              //Call your code here.
              defaultTermStores();
        });
      });
   });
});

function defaultTermStore() {
session = SP.Taxonomy.TaxonomySession.getTaxonomySession(context);
termStore = session.getDefaultSiteCollectionTermStore();
context.load(session);
context.load(termStore);
context.executeQueryAsync(successDefaultTermStore, failedListTaxonomySession);
}

function successDefaultTermStore() {
groups = termStore.get_groups();
context.load(groups);
context.executeQueryAsync(
function () {
var groupList = "List: \n";
var groupsEnum = groups.getEnumerator();
while (groupsEnum.moveNext()) {
var currentGroup = groupsEnum.get_current();
var groupName = currentGroup.get_name();
groupList += groupName + "\n";
}
alert(groupList);
}, function () {
//failed
});
}

If you review the code above, this code loads the SP.Taxonomy.js file, then loads the TaxonomySession from context, calls session.getDefaultSiteCollectionTermStore(). Loads that term store, then gets all groups belonging to that term store, then lastly prints them out to an alert window.

Every time I ran this code I was getting the error message.

The reason why I was getting this message was because the properties of the Managed Metadata Service Connection didn’t have “This service application is the default storage location for column specific term sets” ticked.

You need to go into Central administration > Application Management > Manage service applications. Find your Managed Metadata Service, then select the Managed Metadata Service Connection, and click Properties on the ribbon.

Then tick ensure the second option is ticked.

If you re-run the code you should find it now works.

Using HTML template and JQuery.clone()



In a recent proof of concept I was putting together, I needed a way to display different data on the fly, but with the same layout. As it was for a SharePoint 2013 SharePoint hosted app, all I had was Java Script and HTML to perform my task. Originally when I first started to write the code I was building the DIV, SPAN and other objects in the code, line by line. When I was about to start the code for the 3rd section, I thought there must be an easier way.

And there is! Create a template on the HTML page to clone when needed, but have it hidden.

Below is an image of the template if it was visible on the page.


If we take a look at the HTML for it, you will see it just a simple HTML layout. The div with the ID “details” is where we will be cloning the template to in code. (Please note I’m not a designer and there are probably huge mistakes with this. Happy to be corrected).

</pre>
<div id="details"></div>
<div class="hiddenTemplate">
<div id="Template">
<h2><label class="header">This is Header</label></h2>
<div class="buttonDiv"><button class="deleteButton" type="button">Delete</button></div>
<b>ID: </b><span class="itemId">ID</span>

<b>Name: </b><span class="&quot;itemName">Name</span>

<span class="descriptionArea">
<b>Description: </b><span class="itemDescription">Description</span>

</span>
<span class="parentArea">
<b>Parent ID: </b><span class="itemParentID">Parent ID</span>

<b>Parent Name: </b><span class="&quot;itemParentName">Parent Name</span>

</span>
<div class="addItem"><span class="addItemInstruction">Add Items here</span>

<input class="addItemInput" type="text" />
<span class="addButtonSpan"><button class="addButton" type="button">Add</button></span></div>
</div>
</div>
<pre>

Using css I’m able to style and hide the template from the user on the page.

.hiddenTemplate {
display:none;
}
.deleteButton
{
margin-left:0px;
margin-top:2px;
}
.addItem
{
border:1px dotted lightgray
}
.parentArea
{
.display:none;
}

In your JavaScript code, once you have obtained your data you simply grab a copy of the template, clone it using $(‘#Template’).clone(); and then update any details before adding it to the “details” div.

I needed to use the template 4 times and show it slight different for each one, and also depending on values returned. Below is the code for just one of my returned items, however you can see how simple in a loop you could reuse this, or design it differently depending on the data coming back.

 //TermDetails.
 //Get if it is the root term, Term Name and Description.
 var root = termDetails.get_isRoot();
 var termName = termDetails.get_name();
 var termDescription = termDetails.get_description();

//Find template and Clone it.
 var termTemplate = $('#Template').clone();
 //Get the header, and update the text within the clone template.
 termTemplate.find('.header').text("Term Details");
 //Give the template a unique ID, so it can be referenced later in code if needed.
 termTemplate.attr('id', "boxDetails" + termId.toString());

//Show the display button, and give it an on click function.
 termTemplate.find('.deleteButton').show().on('click', function () { removeTerm(termStoreId, groupId, termSetId, termId) });
 //Display Item ID.
 termTemplate.find('.itemId').text(termId.toString());
 //Display Item Name.
 termTemplate.find('.itemName').text(termName);
 //If there is no description don't show it
 if (termDescription == "")
 termTemplate.find('descriptionArea').hide();
 else
 termTemplate.find('itemDescription').text(termDescription.toString());

//If not root, then it has a parent, find out who the parent is.
 var parent;
 if (!root) {
 parent = termDetails.get_parent();
 context.load(parent);
 context.executeQueryAsync(
 function () {
 //success getting parent
 var parentName = parent.get_name();
 var parentId = parent.get_id();
 //Show Parent Area.
 termTemplate.find('.parentArea').show();
 //Display Parent ID.
 termTemplate.find('.itemParentID').text(parentId.toString());
 //Display Parent Name.
 termTemplate.find('.itemParentName').text(parentName);
 }, function () {
 //failed
 console.log("Error obtaining parent details");
 });
 }

//Put instructions for the add button
 termTemplate.find('.addItemInstructions').text("Please enter the name of the Term to add to " + termName);
 //Give input box an ID to reference later
 termTemplate.find('.addItemInput').attr('id', 'createSubTermTextBox');
 //Give button an ID.
 termTemplate.find('.addButton').attr('id', 'createSubTermButton');
 //Change text of button.
 termTemplate.find('.addButton').attr('value', 'Add Term');
 //Give the button an on click function
 termTemplate.find('.addButton').on('click', function () { addSubTerm(termStoreId, groupId, termSetId, termId); });

//Add the template to the details tag.
 $('#details').append(termTemplate);
 

With some additional css the final outcome looks like below.