Skip to main content

Using jquery templates for adding form element dynamically in ASP.NET MVC

Hello friends. Today I wanna tell about some obstucles with validation which I've found when I had working with form.
At first I've created two models for binding

using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using DataAnnotationsExtensions;

namespace DynamicForm.Models
{
    public class FormViewModel
    {
        public int? Id { get; set; }

        [Required]
        [StringLength(25, ErrorMessage = "Max firstname length is 25 symbols.")]
        [DisplayName("First name")]
        public string FirstName { get; set; }

        [Required]
        [StringLength(25, ErrorMessage = "Max lastname length is 25 symbols.")]
        [DisplayName("Last name")]
        public string LastName { get; set; }

        [Required]
        [Email(ErrorMessage = "Provide correct email address, please.")]
        [DisplayName("Email")]
        public string Email { get; set; }

        [Range(16, 150, ErrorMessage = "Age should be between 16 and 150.")]
        [DisplayName("Age")]
        public int? Age { get; set; }

        public IList Discounts { get; set; }
    }
}

and DiscountCode model

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using DataAnnotationsExtensions;

namespace DynamicForm.Models
{
    public class DiscountCode
    {
        [Required]
        [DisplayName("Code name")]
        [StringLength(10, ErrorMessage = "Max name length is 10 symbols.")]
        public string Code { get; set; }

        [Required]
        [DisplayName("Code discount")]
        [Integer(ErrorMessage = "The field Percent should be a positive non-decimal number")]
        [Range(1,60, ErrorMessage = "The field Percent should be between 1 and 60.")]
        public int Percent { get; set; }
    }
}

As you know HTML helpers for rendering inputs generate elements with additional data attributes (data-val, data-val-* and so on)
for unobtrusive client-side validation. I've created template without any data atributes at all. When data has been sent to server in action it will be validated if there is some problems action return back partial view with form to client, but now all inputs which had been added dynamically will be rendered with all data attributes which are needed for unobtrusive validation.

@{
    ViewBag.Title = "Dynamic form";
    Layout = "~/Views/Shared/Layout.cshtml";
}

<h2>Test dynamic form</h2>
<button id="showForm">Show form</button>
<div id="dialog" style="display: none;" title="Dynamic loaded form">
    <div id="formContainer">
        @{ Html.RenderAction("GetForm"); }
    </div>
    <div>
        <button id="AddInput">add element dynamically</button>
    </div>
</div>
@section scripts
{
    <script>
        (function ($) {
            $.fn.clearErrors = function () {
                $(this).each(function () {
                    $(this).find(".field-validation-error").empty();
                    $(this).trigger('reset.unobtrusiveValidation');
                });
            };

            $.fn.resetForm = function () {
                $(this).find('#Discounts > .row').remove();
                $(this).find('input').val('');
                $(this).find('.form-group').removeClass("has-success").removeClass("has-error");
            };

            $.fn.enableValidation = function () {
                $(this).removeData("validator").removeData("unobtrusiveValidation");
                $.validator.unobtrusive.parse($(this));
            };
            $.validator.setDefaults({
                highlight: function (element) {
                    $(element).closest(".form-group").removeClass("has-success").addClass("has-error");
                },
                unhighlight: function (element) {
                    $(element).closest(".form-group").removeClass("has-error").addClass('has-success');
                }
            });
            $.ajaxSetup({ cache: false });
            var dialog,
                dialogParams = {
                    dialogClass: "no-close",
                    minWidth: 800,
                    autoOpen: false,
                    modal: true
                };
            function View(params) {

                function initDialog() {
                    dialog = $(params.dialogSelector).dialog($.extend(dialogParams, {
                        open: function (event, ui) {
                            $('form').clearErrors();
                        },
                        buttons: {
                            "Close": function () {
                                dialog.dialog('close');
                            },
                            "Save": function () {
                                if ($('form').valid()) {
                                    $.ajax({
                                        url: $('form').attr('action'),
                                        type: "POST",
                                        data: $('form').serialize(),
                                        success: function (result) {
                                            if (result.success) {
                                                $('form').resetForm();
                                                $(dialog).dialog('close');
                                            } else {
                                                $("#formContainer").html(result);
                                                $('form').enableValidation();
                                            }
                                        }
                                    });
                                }
                            }
                        }
                    }));
                }

                function showDialog() {
                    initDialog();
                    dialog.dialog('open');
                }

                function bindEvents() {
                    $(params.showFormButtonSelector).on('click', function () {
                        showDialog();
                    });

                    $(params.dialogSelector).on('click', '#AddInput', function () {
                        var $discountsContainer = $("#Discounts"),
                             lastIndex = 0,
                            data, html;
                        if ($discountsContainer.find('.row:last').length > 0) {
                            //provide correct index for non-sequential binding
                            lastIndex = parseInt($discountsContainer.find('.row:last > input[name="Discounts.Index"]').val(), 10) + 1;
                        }
                        data = { index: lastIndex };
                        html = $.templates("#discountRow").render(data);
                        $(html).appendTo($discountsContainer);
                        $('form').enableValidation();
                    });

                    $(document).on('click', '.removeDiscountRow', function (e) {
                        $(e.target).parents('.row').remove();
                    });
                }

                return {
                    bindEvents: bindEvents
                }
            }

            $(function () {
                var view = new View({
                    dialogSelector: "#dialog",
                    showFormButtonSelector: "#showForm"
                });
                view.bindEvents();
            });
        })(jQuery);
    </script>
}
<script id="discountRow" type="text/x-jsrender">
    <div class="row">
        <input type="hidden" name="Discounts.Index" value="{{: index}}">
        <div class="col-md-4 form-group">
            <div class="input-group">
                <label class="control-label" for="Discounts_{{: index}}__Code">Code name</label>
                <input class="form-control" id="Discounts_{{: index}}__Code" name="Discounts[{{: index}}].Code" type="text" />
            </div>
        </div>
        <div class="col-md-6 form-group">
            <div class="input-group">
                <label class="control-label" for="Discounts_{{: index}}__Percent">Code discount</label>
                <input class="form-control" id="Discounts_{{: index}}__Percent" name="Discounts[{{: index}}].Percent" type="text" />
            </div>
        </div>
        <div class="col-md-2 form-group">
            <div class="input-group">
                <button type="button" class="btn btn-primary removeDiscountRow">Remove</button>
            </div>
        </div>
    </div>
</script>

<style>
    .removeDiscountRow {
        margin-top: 25px;
    }
</style>
I've written simple Controller for this example
using System.Web.Mvc;
using DynamicForm.Models;

namespace DynamicForm.Controllers
{
    public class IndexController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        [HttpGet]
        public ActionResult GetForm()
        {
            return PartialView("_Form", new FormViewModel());
        }

        [HttpPost]
        public ActionResult SaveForm(FormViewModel formviewModel)
        {
            if (!ModelState.IsValid)
            {
                return PartialView("_Form", formviewModel);
            }
            return Json(new { success = true });
        }
    }
}
The form you can see below
viewModel under debugger
All code you can find at github: GitHub

Comments

Popular posts from this blog

Some notes about transportation problem

Hello guys. After work I remembered my studying at university. My first thoughts is about solving Monge–Kantorovich transportation problem using a modification of simplex method known as Method of Potentials. Transportation theory investigates methods for optimal allocation resources among consumers and transportation them with minimum cost. For example, suppose we have some factories which provide materials and shops which consume it. (To be continued)

New Personal Website

Hello World, hello my blog again. I have good news, finally, I created my website , I do not know exactly what is the main goal for that and how it will be updated. Now I want to sync my current posts from the blogger platform and personal website, it can be easily done by using the `Blogger API` with some extra requirements which I want to have.   Especially,   - able to show/hide the post separately from blogger (1)   - use custom posts order (2)   - add some extra information besides tags (3)    For instance, technology stack, team size, etc. - add a possibility to use any 3rd party code highlighter instead of post-computed HTML from blogger (4) I think that the 4 the most required and nice feature - able to sync manually, or use cron for syncing with blogger (5)   There is some initial schema of my application   This is a pretty simple solution: two go services, one for fetching data, checking referential integr...