5

I am trying to create an AutoComplete textbox, while using an EditorTemplate. The problem I am facing is that by using the Html.BeginCollectionItem() extension solution (https://www.nuget.org/packages/BeginCollectionItem/), the Id's of the EditorFor() and TextBoxFor() methods get set dynamically and this breaks my javascript. Next to that, I do not exactly know if this is even possible (and if so, how. Below you will find how far I have come).

In the main view I have a loop to generate a partial view for each item in a collection

for (int i = 0; i < Model.VoedingCollection.Count; i++)
{
    @Html.EditorFor(x => x.VoedingCollection[i], "CreateVoedingTemplate")
}

The partial view CreateVoedingTemplate.cshtml uses the Html.BeginCollectionItem() method

@using (Html.BeginCollectionItem("VoedingCollection"))
{
    string uniqueId = ViewData.TemplateInfo.HtmlFieldPrefix.Replace('[', '_').Replace(']', '_').ToString();
    string searchId = "Search_";
    string standaardVoedingId = "StandaardVoeding_";
    foreach (KeyValuePair<string, object> item in ViewData)
    {
        if (item.Key == "Count")
        {
            searchId = searchId + item.Value.ToString();
            standaardVoedingId = standaardVoedingId + item.Value.ToString();
        }
    }

    <div class="form-horizontal">   
       <div class="form-group" id=@standaardVoedingId>
            @Html.LabelFor(model => model.fk_standaardVoedingId, "Voeding naam", htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.HiddenFor(model => model.fk_standaardVoedingId)
                    <input type="text" id='@searchId' placeholder="Search for a product"/>
                </div>
       </div>
    </div>

    <script type="text/javascript">
        var id = '@uniqueId' + '_fk_standaardVoedingId'.toString();
        var search = '@searchId'.toString();

        var url = '@Url.RouteUrl("DefaultApi", new { httproute = "", controller = "AgendaApi" })';

        $(document.getElementById(search)).autocomplete({
            source: function (request, response) {
                $.ajax({
                    url: url,
                    data: { query: request.term },
                    dataType: 'json',
                    type: 'GET',
                    success: function (data) {
                        response($.map(data, function (item) {
                            return {
                                label: item.standaardVoedingNaam,
                                value: item.standaardVoedingId
                            }
                        }));
                    }
                })
            },
            select: function (event, ui) {
                $(document.getElementById(search)).val(ui.item.label);
                //$('#id').val(ui.item.value);
                document.getElementById(id).value = ui.item.value;
                return false;
            },
            minLength: 1
        });
    </script>
}

<link href="~/Content/SearchBox/jquery-ui.css" rel="stylesheet" />
<script src="~/Scripts/SearchBox/jquery-1.9.1.js"></script>
<script src="~/Scripts/SearchBox/jquery-ui.js"></script>

In the script above, I am trying to make a function where the user can type in the name of a standaardVoeding item and then get results, where, after the user selects a standaardVoeding item, the standaardVoedingId property gets set. Then, after submitting the whole form, the controller receives the standaardVoedingId (with all the other info as well)

So I guess Javascript somehow cannot handle the Razor View @ code and, next to that, Html.BeginCollectionItem does something fishy because you cannot set the value of its textboxes via code during runtime. Next to that, I have tried doing alert(document.getElementById(*html.begincollectionitemId*)) and it finds the fields fine. But apparently all other methods do not work?

Is there perhaps a better solution to getting this to work?

DaGrooveNL
  • 177
  • 1
  • 1
  • 10
  • Sorry ! What is exactly is the problem you are facing ? – Shyju Aug 30 '16 at 02:22
  • Just use a class name to attach the plugin (not `id` attributes) –  Aug 30 '16 at 02:33
  • The problem I am facing is that with the written code here, the action does not get send to the controller. Next to that, how do I bind the data I get back when setting the value of a Html.BeginCollectionItem field by javascript apparently does not work? So, basically I want an autocomplete form which returns a value and binds that value to a property of a model. – DaGrooveNL Aug 30 '16 at 12:48
  • Stephen Muecke, how would I bind the found data to a model's property and retrieve that data alongside the other data in the controller on postback? Do I have to use ViewBag? – DaGrooveNL Aug 30 '16 at 12:50
  • @DaGrooveNL, To ping a user start the message as I did here. Sorry to be harsh, but there is so much bad code here its hard to understand what your wanting to do. Are you wanting to attach a jquery.autocomplete to the textbox and then update the hidden input with the value of the selected `standaardVoedingId` property? –  Aug 30 '16 at 23:49
  • And can you confirm that you are dynamically adding new items to you `VoedingCollection` collection property (i.e that you have a 'Add' button in the main view that makes an ajax call to a method to add a new item by updating the DOM? –  Aug 30 '16 at 23:54
  • @StephenMuecke, The VoedingCollection collection property gets created dynamically, but not by using an 'Add' button. To add the VoedingCollection, I use a for-loop in the main view; @Html.EditorFor(x => x.VoedingCollection[i], "CreateVoedingTemplate") . And yes, I am trying to make a function where the user can type in the name of a standaardVoeding item and then get results, where, after the user selects a standaardVoeding item, the standaardVoedingId property gets set. Then, after submitting the whole form, the controller receives the standaardVoedingId (with all the other info as well). – DaGrooveNL Sep 01 '16 at 15:05
  • That is not _dynamically adding items_ (which refers to adding collection items in the view) and there is no point at all using the `BeginCollectionItem()` helper method - its just unnecessary extra overhead - and you would be better off just using a `for` loop or custom `EditorTemplate` for adding the controls (refer [this answer](http://stackoverflow.com/questions/30094047/html-table-to-ado-net-datatable/30094943#30094943)). In anycase, I will add an answer later showing you how this should be done using class names and relative selectors –  Sep 01 '16 at 21:44
  • Awesome, cheers! I will check out your mentioned method of using an other solution instead of BeginCollectionItem. – DaGrooveNL Sep 02 '16 at 04:03
  • Have just added answer, but will update it in a few hours to show how it should be modified since you do not really need `BeginCollectionItem()`. Can you also edit the question to include your previous comments which makes it clear how your using this in the main view (and then I will remove the down-vote) –  Sep 02 '16 at 04:13

1 Answers1

1

The BeginCollectionItem() method alters the id and name attributes of the html generated by the inbuilt helpers, in your case, for the hidden input, instead of

<input ... name="fk_standaardVoedingId" .... />

it will generate

<input ... name="VoedingCollection[xxxx].fk_standaardVoedingId" .... />

where xxxx is a Guid.

While it would be possible to use javascript to extract the Guid value from the textbox (assuming that was generated correctly usind @Html.TextBoxFor()) and build the id of the associated hidden input to use as a selector, it is far easier to use class names and relative selectors.

You also need to remove your scripts and css from the partial and put that in the main view (or its layout). Apart from the inline scripts, your duplicating it for each item in your collection.

Your partial needs to be

@using (Html.BeginCollectionItem("VoedingCollection"))
{
    <div class="form-horizontal">   
       <div class="form-group">
            @Html.LabelFor(model => model.fk_standaardVoedingId, "Voeding naam", htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10 item"> // add class name
                @Html.HiddenFor(model => model.fk_standaardVoedingId)
                <input type="text" class="search" placeholder="Search for a product"/>
            </div>
       </div>
    </div>
}

Note the class name for the textbox and its container which also includes the hidden input. Then in the main view, the script will be

<script type="text/javascript">
    var url = '@Url.RouteUrl("DefaultApi", new { httproute = "", controller = "AgendaApi" })';
    // Attach the script to all textboxes
    $('.search').autocomplete({
        source: function (request, response) {
            $.ajax({
                url: url,
                data: { query: request.term },
                dataType: 'json',
                type: 'GET',
                success: function (data) {
                    response($.map(data, function (item) {
                        return {
                            label: item.standaardVoedingNaam,
                            value: item.standaardVoedingNaam, // this needs to be the name
                            id: item.standaardVoedingId // add property for the id
                        }
                    }));
                }
            })
        },
        select: function (event, ui) {
            // Get the associated hidden input
            var input = $(this).closest('.item').find('input[type="hidden"]');
            // Set the value of the id property
            input.val(ui.item.id);
        },
        minLength: 1
    });
</script>

Based on your comments that your are not dynamically adding or removing items in the view, then there is no point in the extra overhead or using the BeginCollectionItem() method. Change the name of your partial to standaardvoeding.cshtml (assuming that's the name of the class) and move it to the /Views/Shared/EditorTemplates folder.

Then in the main view, replace your for loop with

@Html.EditorFor(m => m.VoedingCollection)

which will generate the correct html for each item in the collection. Finally remove the BeginCollectionItem() method from the template so that its just

<div class="form-horizontal">   
   <div class="form-group">
        @Html.LabelFor(m => m.fk_standaardVoedingId, "Voeding naam", htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10 item"> // add class name
            @Html.HiddenFor(m => m.fk_standaardVoedingId)
            <input type="text" class="search" placeholder="Search for a product"/>
        </div>
   </div>
</div>
  • your javascript code contains an error, it puts the id of the item which the user selected in the search box instead of the @Html.HiddenFor(m => m.fk_standaardVoedingId) – DaGrooveNL Sep 02 '16 at 15:48
  • What? `var input = $(this).closest('.item').find('input[type="hidden"]);` gets the hidden input and `input.val(ui.value);` set the value! And its your code ( the `return { label: item.standaardVoedingNaam, value: item.standaardVoedingId }`) which does that. I just copied it because I assumed that was what you wanted. –  Sep 02 '16 at 22:00
  • You also have an error in the javascript code, you misstyped a ' near input. It does not work. I tried, it does not set the value. My question was how to set the hidden field with the ID that the user retrieves by typing something, then subsequently selecting an item and after that, the hidden field gets set. The normal search box should also be set to the value (not the ID) which the user selected. – DaGrooveNL Sep 02 '16 at 23:00
  • Again you are a lifesaver. Thank you so much! – DaGrooveNL Sep 03 '16 at 00:16
  • One small comment though, there is still an error near '.search; It should be '.search' But my problem is solved now. – DaGrooveNL Sep 03 '16 at 00:20