Search code examples
jqueryweb-partsjquery-templates

append script containing html for jquery template


Please bear with me as I try to explain this as it's somewhat complicated and I am totally new to JQuery and trying to get my head around it.

My team and I are working on WebParts for a SharePoint project. The WebPart contains a user control (SearchControl), which has a link to open a dialog (using jquery ui) that contains another user control (SelectorControl).

SelectorControl contains:

  • a drop-down list (objTemplates) that is populated using JQuery Templates
  • a table that is populated using another JQuery Template; and
  • a readonly text input to display the currently selected item from the table

SelectorControl code:

        <div class="objSelector">
        <script id="objSelectorTemplate" type="text/x-jquery-tmpl">
        <div class="table">
            <div class="row">
               <div class="labelInputGroup">
                    <label for="objTemplates">Obj Template</label>
                    <select id="objTemplates">
                        {{each ObjectTemplates}}
                            <option value="${ObjectTemplateId}">${Name}</option>
                        {{/each}}
                    </select>
                </div>
            </div>
            <div><br></div>
            <div class="row">
                <div>
                    <table class="tableGrid">
                        <thead>
                            <tr>
                                <th>Object Name</th>
                            </tr>
                        </thead>
                        <tbody id="listPlaceholder">
                        </tbody>
                    </table>
                    <div id="pagerPlaceholder"></div>
                </div>        
            </div>     
            <div id="templatePlaceholder">
            </div>
            <div><br></div>
            <div class="row">
                <input type="hidden" id="localSelectedObjectId" class="hidden" />
                <label for="localSelectedObject">Selected Object</label>
                <input type="text" class="dealName" id="localSelectedObject" readonly="readonly" placeholder="No object selected"/>
            </div>
        </div>
        </script>    
        <script id="listTemplate" type="text/x-jquery-tmpl">
            <tr>
                <td><a class="objectItem" id="${Id}" href="#">${Name}</a></td>
            </tr>
       </script>    
       <div id="pagerControl">
          <uc1:PagerControl ID="PagerControl1" runat="server" />
       </div> 
    </div>

The drop-down list is populated just before the dialog is created using data retrieved from a WCF service that's then bound to the first template.

A 'change' event is bound to the drop-down list using live to populate the table (second template) by calling a WCF Service and passing through the currently selected item in the drop-down list.

When the change event is triggered on the drop-down list, LoadObjects is called:

            function LoadObjects(event) {

            var dialogDom = event.data.DialogDom;
            var searchObj = GetSearchFilters(); // removed irrelevant code here

            var listTemplate = searchDom.find('#listTemplate');
                    var templatePlaceholder = dialogDom.find('#templatePlaceholder');
                    templatePlaceholder.empty();
                    templatePlaceholder.append(listTemplate);

                    var pagerControl = searchDom.find('#pagerControl');
                    var pagerPlaceholder = dialogDom.find('#pagerPlaceholder');
                    pagerPlaceholder.append(pagerControl);

                    var list = GenerateList(
                        dialogDom,
                        $('<div></div>'),
                        searchObj,
                        Connection('MyUiService', 'GetObjects'));
        }

The GenerateList() method creates a JQuery object with various methods including ones that get the objects data from a WCF service and bind it to the template (listTemplate)

The PagerControl code also contains a template, which is populated at the same time as listTemplate.

The code for listTemplate and the pager had to be put outside of the objectSelectorTemplate script and then copied in later to prevent it from being removed when the page is parsed for the populating of the drop-down list.

All of the above works fine the first time the dialog is opened and an item is selected from the drop-down list. It is possible to change pages within the data in the table without any problem but as soon as the selected item in the drop-down list changes, the table data is no longer loaded. If I step through the code, I can see that append() (in the LoadObjects method) removes the listTemplate script when it inserts it into listPlaceholder, which means that when the dialog goes to load the table data again, the script is no longer there. To get around this, I tried appending a clone instead:

    templatePlaceholder.append(listTemplate.clone());

This successfully prevented the listTemplate script from being removed, however, the cloned copy contained the script tags but not its contents so the table data still didn't get loaded. I searched through SO and found John Resig's response to this question:

So I changed the above code to:

    var listTemplateCopy = jQuery.extend({}, listTemplate);
    templatePlaceholder.append(listTemplateCopy);

This successfully copied the script tag and its contents to listTemplateCopy but append() removes listTemplate as well as listTemplateCopy. At first I assumed this was because extend is intended for merging two (or more) objects into a single object but from John's response and from what I've read in the documentation, it sounded like extend is what you're supposed to use to create a copy of a jquery object.

Can anyone explain to me what exactly is going on and recommend a solution to the above?

A couple of ideas I had was to:

  • shift the script tags to only surround the template code. I could then have the second template code in place rather than needing to append it. Binding would be done by binding to the drop-down list template then to the table
  • use a different manner to populate the drop-down list rather than a template to simplify the page and remove the "necessity" to have scripts being appended, as there wouldn't be a script tag surrounding the whole lot

I would like to gain an understanding of exactly what the code is currently doing though before pursuing either of those approaches.


Solution

  • This is not by any means going to be an elegant answer, but essentially, this is a failing of IE8's scripting engine and its predecessors. If your project has requirements that these browsers are supported, then the code's going to get a bit uglier to look at and jQuery won't be much help. You'll notice your first use of .clone() in code actually functions just fine in IE9, Firefox, Chrome, and Safari. Allow me to explain the particular difficulty.

    Your requirement is to replicate a DOM object. $.extend() is inappropriate in this instance as it will simply clone a jQuery object that wraps a DOM target set, rather than the actual DOM target(s). That means that the new copy of the wrapper still has the same DOM element(s) targeted, and instead of appending a copy of the target(s), it moves the target(s). For this reason, your initial assumption to use .clone()was correct. The reason the code is failing to behave as expected is beyond your control.

    IE9 corrects a glaring deficiency with the Node.cloneNode() method of its predecessors where the innerHTML member of the Node object is not preserved in the case of script elements. This is the method used by jQuery's .clone() call. Executing a line of classic JavaScript on any script block in IE8 and earlier will reveal as much:

    alert(document.getElementById("listTemplate").cloneNode(true).innerHTML);
    

    This alert will have content in every other major browser. Not so in IE8 and below. The "true" argument performs a deep copy just to prove a point, but truthfully this is not necessary as script elements have no Nodes in its childNodes collection.

    The workaround is ugly and I'm not entirely certain it will function as intended since I don't have your system to test with. You will have to explicitly modify the innerHTML attribute of your placeholder element. One thing you might consider is including a defer attribute which tends to make script blocks behave better when dynamically inserted in IE (see MSDN - innerHTML Property at, "When using innerHTML to insert script, you must include the DEFER attribute in the script element"). It would make your JavaScript look something like the following:

    var listTemplate = searchDom.find('#listTemplate');
    var myHtml = '<script defer="defer" type="text/x-jquery-tmpl">';
    myHtml += listTemplate.html();
    myHtml += "</script" + ">";
    
    var templatePlaceholder = dialogDom.find('#templatePlaceholder');
    templatePlaceholder.html(myHtml);
    

    I've made assumptions that listTemplate and templatePlaceholder are jQuery objects so if I am mistaken, I apologize. However, the basic idea here is literally building the full block of script as an HTML string and replacing the HTML inside your placeholder with the string you've built.

    I cannot guarantee IE will interpret the script block correctly and make it available to your jQuery template, but inspection of the innerHTML of a victim placeholder of my choosing did reveal that, at the very least, it was replaced correctly with the generated script block.

    I'd be interested to know if this works.