1

Ok, this is a complicated question and I'm so new at this, maybe I don't even know how to ask it properly.

I'm building an app using ASP.NET MVC. I have a view that contains a form that has an textbox input that asks for a user to name a recipient to their estate for a will. Below the input, I want a button ("Add another recipient") that adds another input to the form.

Here's how I've mocked this up using Javascript (Click "Yes" and then click "Cash" to see what I'm talking about): https://jsfiddle.net/cinerobert/409k27ot/7/

<p>Would you like to leave specific gift in your will?</p>
<p>
  <button onclick="q1AnswerYes()">Yes</button>
  <button onclick="q1AnswerNo()">No</button>
</p>
<br />
<!-- If answer to Q1 is Yes show this block -->
<div id="q1AnswerYesBlock" style="display:none; text-align:center;
margin:auto;">
  <div>What specific gifts would you like to leave?</div>
  <hr>
  <div>
    <button onclick="q2AnswerCash('q2AnswerCashDisp')">Cash</button>
    <button>Car</button>
    <button>Real Estate</button>
    <button>Jewelry</button>
    <button>Other</button>
  </div>
  <div id="q2AnswerCashDisp">
    <!-- Recipient forms get added here -->
  </div>
  <div style="text-align:center;
margin:auto; display:inline">
    <button id="addCashRecipient" style="display:none" onclick="addCashRecipientForm('q2AnswerCashDisp')">Add another recipient</button>
  </div>
</div>

<!-- If answer to Q1 is No show this block -->
<div id="q1AnswerNoBlock" style="display:none">
  <p>Some other crap</p>
</div>
<div id="testFormHTML" style="display:none">
  <form id="testFormId" method="get" style="border-syle:none">
    <fieldset id="fieldsetid" style="border-style:none">
      <div class="formPrompt">
        Amount:
      </div>
      <input type="text">
      <div class="formPrompt">
        Recipient:
      </div>
      <input onblur="submit()" type="text" name="recipient">
      <input style="display:none" class="testFormParamInputId" name="testFormParam" value="">
      <br>
      <br>
    </fieldset>
  </form>
</div>

<script>
var cashRecipientNum = 1;
function q1AnswerYes() {
  document.getElementById("q1AnswerYesBlock").style.display = "block";
  document.getElementById("q1AnswerNoBlock").style.display = "none";
}

function q1AnswerNo() {
  document.getElementById("q1AnswerYesBlock").style.display = "none";
  document.getElementById("q1AnswerNoBlock").style.display = "block";
}

function q2AnswerCash(blockId) {
  if (cashRecipientNum == 1) {
    addForm(blockId, "testForm", cashRecipientNum++);
    document.getElementById("addCashRecipient").style = "display:inline";
  };
}

function addCashRecipientForm(blockId) {
  addForm(blockId, "testForm", cashRecipientNum++);
}

// Function to add a new form with unique action argument to the document.
function addForm(blockId, formArg, numInstance) {
  var formHTML = formArg + "HTML";
  var formId = formArg + "Id";
  var formParamInputId = formArg + "ParamInputId";
  var newFormId = formArg + "Id" + numInstance;
  var div = document.getElementById(formId),
    clone = div.cloneNode(true); // true means clone all childNodes and all eventhandlers
  clone.id = newFormId;
  clone.style = "border-style:none";
  document.getElementById(blockId).appendChild(clone);
  var x = document.getElementById(newFormId).getElementsByClassName(formParamInputId);
  x[0].value = numInstance;

  console.log(x[0].class);
  console.log("here");
}    
</script>

What I can't wrap my head around is this:

  • since I'm using a hardcoded html form (using tags) rather than html helpers (like @Html.TextBoxFor) each input isn't bound to anything in the model. I can read the inputs in using FormCollection, but when the form submits I don't know how to repost the form with the values the user entered still in place.

  • if I use html helpers (@Html.TextBoxFor) and bind each input to a property in the model, then I don't understand how to allow the form to add an unlimited number of input fields

I've searched around for examples of dynamic views, but the examples I've found have to do with a view that responds to changes in the model. I haven't found one that involves adding an unlimited number of input fields based on a user action.

I know this is kind of a shaggy dog of a question, but if someone could help point me in the right direction I would be very grateful. Thank in advance for being patient with a newbie.

Robert M.
  • 575
  • 5
  • 17
  • 2
    It sounds like you need a view model which has a `List` property. What you'd need for your `` tags is a name of `recipients[n]` for each recipient in the form, starting with 0. The MVC model binder will pickup on the array indexing style and use that to bind to the model's recipents property (`List`). I think if you were to post with multiple `recipients` inputs without the indexing I think that would also bind it as well – willwolfram18 Nov 09 '16 at 19:09
  • Ahh, very interesting. Do you mean like: ? – Robert M. Nov 09 '16 at 19:11
  • 1
    I just did a quick example, inputs with either `` or "index" inputs ` ` will work. You just need to make sure you use the same casing in your view model. You'll also need to update your view to use the model and have that populate the inputs using `@Html.InputFor` (or `TextBoxFor`) to preserve the post's values – willwolfram18 Nov 09 '16 at 19:16
  • 1
    For dynamically adding collection items that are complex objects, refer [this answer](http://stackoverflow.com/questions/28019793/submit-same-partial-view-called-multiple-times-data-to-controller/28081308#28081308) - the answer you have accepted will not work because you need to generate a collection of an object containing at least 2 properties (`Recipient` and `Amount`) –  Nov 09 '16 at 21:55
  • Thank you Stephen. In the example you linked, am I correct in thing that AdminProductDetailModel would be a ArrayList collection declared like: class AdminProductDetailModel : ArrayList { public string fkConfigChoiceCategoryColorId; etc. } ? – Robert M. Nov 10 '16 at 18:20
  • Or would class AdminProductDetailModel : IList { ... } be better? – Robert M. Nov 10 '16 at 18:37
  • I'm really confused. How do you make a model class that's a collection of another class? – Robert M. Nov 10 '16 at 20:50
  • @RobertM. To notify a user, start it as this does. I have no idea what your models are, but your main view model needs `public List Recipients { get; set; }` where `xx` is a view model containing properties for the recipient and the amount –  Nov 10 '16 at 22:42
  • Ok, thank you for your patience. I think I understand. If my view model is RecipientModel, in the partial view, would I use something like @Html.LabelFor(model => model.Recipient.name)? – Robert M. Nov 10 '16 at 23:47
  • Also, it doesn't seem like BeginCollectionItem is inserting the hidden tag it's supposed to... – Robert M. Nov 10 '16 at 23:47
  • I made a new question on this so I could post up my code. It's almost working but the model isn't getting passed back to the post method. If you have the time to look at it I would be very grateful. http://stackoverflow.com/questions/40539321/a-partial-view-passing-a-collection-using-the-html-begincollectionitem-helper – Robert M. Nov 11 '16 at 00:23

1 Answers1

1

The scenario you're describing screams View Model. Even a simple one like this would help

public class WillRecipientsViewModel
{
    public List<string> Recipients { get; set; }
}

Then your controller would need the corresponding action of your form

// Controller
public ActionResult SetWillRecipients()
{
    // Added this to force at least 1 text box into the view
    WillRecipientsViewModel model = new WillRecipientsViewModel
    {
        Recipients = new List<string> { "" };
    };
    return View(model);
}

[HttpPost]
public ActionResult SetWillRecipients(WillRecipientsViewModel model)
{
    // Business logic

    // Go back to the view if an error occurs
    return View(model);
}

// View
@model WillRecipientsViewModel
@using(Html.BeginForm("SetWillRecipients"))
{
    @Html.DisplayFor(model => model.Recipients)
    for (int i = 0; i < Model.Recipients.Count; i++)
    {
        // Generate the textboxes for any values that were previously submitted
        Html.InputFor(model => model.Recipients[i])
    }
}

This will bind the values in your form to the Model object which will make things easier. Then you just need to make sure your view is using <input type="text" name="Recipients" /> to capture the data in the model object in your post. I would also recommend doing your best to have the input name attributes closely match the casing of the view models to avoid and conflicts

Updated

I included some more code to exemplify how you can pull the previous recipients into the interface

willwolfram18
  • 1,747
  • 13
  • 25
  • Ok, now I'm confused about something else. Can javascript on the page alter the @html elements? It doesn't seem like it since they render as html on the browser end. And related: how can I have the changes the javascript makes to the form (the new form fields) be retained after post? – Robert M. Nov 09 '16 at 20:08
  • 1
    re:`@Html` rendered elements: by the time the page hits the browser, the elements are already standard HTML, javascript has 0 problems interfacing with, and modifying those tags. I'll update my answer on getting the form fields into the view – willwolfram18 Nov 09 '16 at 21:02
  • Thanks so much. I owe you. I still don't quite understand how the changes javascript makes to the page will persist after post. Doesn't the page get reloaded? – Robert M. Nov 09 '16 at 22:10