1

I'm using the following code to dynamically add and remove a partial view from my MVC form. My problem is that the partial view includes a drop down list that I use Select2 on. When I add a new partial view item, the Select2 initialiser is not called.

In my main form:

@Html.LinkToAddNestedForm(m => m.QuoteItemViewModels, "Add Item ", "#quoteItems tbody", ".quoteItem", "QuoteItemViewModels")

My helper function:

public static MvcHtmlString LinkToAddNestedForm<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string linkText, string containerElement, string counterElement, string cssClass = null) where TProperty : IEnumerable<object>
    {
        // a fake index to replace with a real index
        //long ticks = DateTime.UtcNow.Ticks;
        string fakeIndex = Guid.NewGuid().ToString();
        // pull the name and type from the passed in expression
        string collectionProperty = ExpressionHelper.GetExpressionText(expression);
        var nestedObject = Activator.CreateInstance(typeof(TProperty).GetGenericArguments()[0]);

        // save the field prefix name so we can reset it when we're doing
        string oldPrefix = htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix;
        // if the prefix isn't empty, then prepare to append to it by appending another delimiter
        if (!string.IsNullOrEmpty(oldPrefix))
        {
            htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix += ".";
        }
        // append the collection name and our fake index to the prefix name before rendering
        htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix += string.Format("{0}[{1}]", collectionProperty, fakeIndex);
        string partial = htmlHelper.EditorFor(x => nestedObject).ToHtmlString();

        // done rendering, reset prefix to old name
        htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix = oldPrefix;

        // strip out the fake name injected in (our name was all in the prefix)
        partial = Regex.Replace(partial, @"[\._]?nestedObject", "");

        // encode the output for javascript since we're dumping it in a JS string
        partial = HttpUtility.JavaScriptStringEncode(partial);

        // create the link to render
        var js = string.Format("javascript:addNestedForm('{0}','{1}','{2}','{3}');return false;", containerElement, counterElement, fakeIndex, partial);
        TagBuilder a = new TagBuilder("a");
        a.Attributes.Add("href", "javascript:void(0)");
        a.Attributes.Add("onclick", js);
        if (cssClass != null)
        {
            a.AddCssClass(cssClass);
        }
        a.InnerHtml = linkText;

        return MvcHtmlString.Create(a.ToString(TagRenderMode.Normal));
    }

My JS:

function addNestedForm(container, counter, ticks, content) {
    //var nextIndex = $(counter).length;
    var nextIndex = $(container + " " + counter).length;
    var pattern = new RegExp(ticks, "gi");
    content = content.replace(pattern, nextIndex);
    $(container).append(content);

    resetValidation();
}

function resetValidation() {
    // can't add validation to a form, must clear validation and rebuild
    $("form").removeData("validator");
    $("form").removeData("unobtrusiveValidation");
    $.validator.unobtrusive.parse("form");

}

My partial view:

<tr class="quoteItem">

<td style="padding:1px">

    @Html.DropDownListFor(
     x => x.QuoteItem.Location,
                Model.LocationTypes, "", new { @style = "width:100px", @class = "ddl" })
    @Html.ValidationMessageFor(model => model.QuoteItem.Location)
</td>
<td style="padding:1px">
    @Html.TextBoxFor(x => x.QuoteItem.Area, new { style = "width:100px;height:35px" })
</td>

</tr>

I initialise Select2 with the following code in a js file loaded from a bundle in my layout.cshtml:

$(document).ready(function () {

    $(".ddl").select2();

});

This works on my first item, but not on any subsequently created.

Robbie Mills
  • 2,705
  • 7
  • 52
  • 87

1 Answers1

2

$(document).ready() is triggered once the DOM is fully loaded, as it comes from the server the first time, so it's not triggered when you add/remove the nodes corresponding to your partial view, because you're modifying the tree, not loading it again.

Then, all you have to do is to make sure to execute $(".ddl").select2() after you add the DOM nodes of your partial view. There's no need to reinitialize JQuery to do this.

If I were you, I would just add $(".ddl").select2(); to the end of the addNestedForm() function.

function addNestedForm(container, counter, ticks, content) {
    //var nextIndex = $(counter).length;
    var nextIndex = $(container + " " + counter).length;
    var pattern = new RegExp(ticks, "gi");
    content = content.replace(pattern, nextIndex);
    $(container).append(content);

    resetValidation();

    $(".ddl").select2();
}

An alternate solution is to add a listener, as seen in this post: Is there a JavaScript/jQuery DOM change listener?

Community
  • 1
  • 1
dsnunez
  • 827
  • 6
  • 14
  • absolutely correct, but to really earn a bounty / accepted answer, add example code. Good luck. – Dave Alperovich Jun 04 '14 at 00:21
  • THanks for that, and seems to make sense, except when I add $(".ddl").select2(); to the end of the addNestedForm() function and then click 'add item', I get the following error in my console: Uncaught query function not defined for Select2 s2id_QuoteItemViewModels_0__QuoteItem_Location – Robbie Mills Jun 04 '14 at 02:39
  • @RobbieMills, that's interesting. can you please add your `select2()` method to your Original Post? – Dave Alperovich Jun 05 '14 at 10:43
  • @RobbieMills, you may want to see this post http://stackoverflow.com/questions/14483348/query-function-not-defined-for-select2-undefined-error – dsnunez Jun 05 '14 at 13:23
  • Thanks @dsnunez, that post along with your example helped. – Robbie Mills Jun 05 '14 at 22:59