Search code examples
asp.net-coreasp.net-core-mvcrazor-pagesunobtrusive-validation

Adding dynamically list elements to form and Client Side Validation


I want to add elements of class type Food to a form. When the user clicks some button a row with two elements should be displayed:

  • one dropdown list to choose type of food
  • an input element for the amount. When the form is posted to the backend, the dynamically added elements need to bind to a List property named FoodItems.
public class Food {
    public int SelectedFoodItemId {get; set;}
    public int Amount {get; set;}
}

My Razor page has the following:

[Bind Property]
public InputModel Input {get; set;}

public class InputModel{
    public List<Food> FoodItems {get; set;}
    public List<SelectListItem> FoodItemsSelectList {get; set;}
    
    // Other properties to be displayed left out for brevity
}

How should I add new elements to the page and also enable the client side validation (using the default JQuery Validation and Unobtrusive Validation) in a way that the added elements properly bind to the list(FoodItems) on the backend ?

I have tried to implement this using javascript in a way that I have a variable counter that tracks how many elements(rows of Food data) are displayed to the user and using that and Ajax call to controller(had to add MVC to the Program) action method to get Json data for the FoodItemsSelectList (that populate the dropdown list) generate new rows of Food data by manipulating the Id and Name fields so that backend Model Binding and Client side re-enabling is working properly. It works but I feel it is a lot of work and that there is a easier way of doing it.


Solution

  • How should I add new elements to the page and also enable the client side validation (using the default JQuery Validation and Unobtrusive Validation) in a way that the added elements properly bind to the list(FoodItems) on the backend ?

    Well, based on your scenario and description, this can be done done in many ways. One of the easiest way is to use javascript addEventListener with the form submit even.

    When you would click on from submit button there's should be an function which would create a dynamic from using raw html string interpolation using javascript. Within that function , you can set the validation functionality as well.

    Let's have a look how we can achieve that.

    In order to simulate your scenario, I have used following way:

    Razor.cs page:

    public class FoodModel : PageModel
    {
        [BindProperty]
        public InputModel Input { get; set; }
    
        public class InputModel
        {
            public List<Food> FoodItems { get; set; }
            public List<SelectListItem> FoodOptions { get; set; }
        }
    
        public class Food
        {
            public int SelectedFoodItemId { get; set; }
            public int Amount { get; set; }
        }
    
        public void OnGet()
        {
            
            var foodOptions = new List<SelectListItem>
            {
                new SelectListItem { Value = "1", Text = "Option 1" },
                new SelectListItem { Value = "2", Text = "Option 2" },
                new SelectListItem { Value = "3", Text = "Option 3" }
                
            };
    
            Input = new InputModel
            {
                FoodItems = new List<Food>(),
                FoodOptions = foodOptions
            };
        }
    
        public IActionResult OnPost(IFormCollection formData)
        {
           
    
            var dynamicFormDataDictionary = new Dictionary<string, object>();
    
            foreach (var item in formData)
            {
    
                dynamicFormDataDictionary.Add(item.Key, item.Value);
                dynamicFormDataDictionary.Remove("__RequestVerificationToken");
            }
    
            return Page();
        }
    }
    

    Razor.cshtml page:

    @page
    @model RazorPageDemoApp.Pages.FoodModel
    
    <form method="post">
        <div id="foodItemsContainer">
            
        </div>
        <br />
        <button type="button" id="addFoodRow" class="btn btn-primary">Add Food Row</button>
    
        <button type="submit" class="btn btn-success">Submit</button>
    </form>
    
    @section Scripts {
        <script>
            document.addEventListener("DOMContentLoaded", function () {
                const addFoodRowButton = document.getElementById("addFoodRow");
                const foodItemsContainer = document.getElementById("foodItemsContainer");
                let counter = 0;
    
                addFoodRowButton.addEventListener("click", function () {
                    counter++;
    
                    const rowHtml = `
                                <div class="food-row">
                                    <select name="Input.FoodItems[${counter}].SelectedFoodItemId" class="food-dropdown" required>
                                       @foreach (var option in Model.Input.FoodOptions)
                                            {
                                                <option value="@option.Value">@option.Text</option>
                                            }
                                    </select>
                                    <input type="number" name="Input.FoodItems[${counter}].Amount" class="food-amount" required />
                                    <div class="invalid-feedback">This field is required.</div>
                                </div>
                            `;
    
                    foodItemsContainer.insertAdjacentHTML("beforeend", rowHtml);
                });
    
                // Validation for dynamically added elements
                document.querySelector('form').addEventListener('submit', function (event) {
                    if (foodItemsContainer.children.length === 0) {
                        event.preventDefault();
                        alert("You haven't added any data.");
                    } else {
                        const invalidFields = this.querySelectorAll(':invalid');
                        if (invalidFields.length > 0) {
                            event.preventDefault();
                            invalidFields.forEach(function (field) {
                                field.classList.add('is-invalid');
                            });
                        }
                    }
                });
            });
        </script>
    }
    

    Output:

    enter image description here

    enter image description here

    enter image description here

    Note: If you want know some other ways, please share your question with more specific details in new post. In addition, you can have a look in this official document.