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)

Університет нафти і газу

Всім привіт. Я поступив в національний технічний університет Нафти і Газу. Поступив на 2 курс, хоча мав б бути на третьому. Я дякую, за те, що поступив на другий курс на держ. форму. Але не все так просто. Потрібно перезаразувати години предметів, які вчили в універі на 1 і частково на 2 курсі, для того щоб без проблем перейти на 3 курс. На рахунок програмування, майже нічого нового немає. Хіба що будем вчити Java на на предметі "технологія розробки ПЗ". Ось і все, що я хотів написати. Всім удачі!

Nginx rewrite rules for hosting angular(any) static app on subroute

Hi everyone, I decided to reorganise my personal blog and use it mostly as "Notes" Today, I want to host two websites served by golang application. The simplest solution is to create system service which points to go executable app on the particular port. In a nutshell, there are two separate services which use 9990 and 9991 ports. Next step I added this simple rule to the Nginx location /admin { rewrite /admin/(.*) /$1 break; proxy_pass http://localhost:9991; proxy_redirect off; proxy_set_header Host $host; } location / { proxy_pass http://localhost:9990; } So this simple configuration just rewrites URL properly to the admin service and I do not need to add extra `admin` route prefix. The latest step is set-up base href for the static `index.html` file. We can easily do it with build option `ng build --base-href /admin/` Thank everyone for reading.