Question

I have a form that represents an item. The form contains a submit button. If the submit button is clicked, validation unobtrusive validation on these fields should occur.

If the validation fails, nothing else should happen.

If the validation passes, the item should be added to a Knockout.js observedArray collection.

In both cases, the entire process should remain on client side without a submission to the server. Submission and server side validation will take place at a later stage of the process.

How can I achieve the desired effect?

I am using ASP.Net MVC with Data Annotations. I prefer not to manually duplicate validation logic on the client side.

I should also mention that I have several forms on the same page.

Here is what I have done this far...

Here is my ASP.Net MVC layout file:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - JC Guns Online</title>


    @*---------- Stylesheets ----------*@

    @Styles.Render("~/Content/Bootstrap/bootstrap-theme.css")
    @Styles.Render("~/Content/MightyIT/bootstrap_customizations.css")
    @Styles.Render("~/Content/site.css")
    @Styles.Render("~/Content/MightyIT/custom_styles.css")
    @Styles.Render("~/Content/MightyIT/callout.css") 
    @Styles.Render("~/Content/font-awesome-4.0.3/css/font-awesome.min.css")
    @RenderSection("css", required: false)


</head>
<body>

    <div class="navbar navbar-default navbar-fixed-top">
        <div class="container">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">

                <li>@Html.ActionLink("Home", "Index", "Home")</li>
                <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
                @*<li>
                    @using (Html.BeginForm())
                    {
                        <input id="txtQuickSearch" type="text" class="form-control col-lg-8" placeholder="Search">
                        <img src="~/Content/img/search_32.png" />
                    }
                </li>*@
            </ul>
            @Html.Partial("_LoginPartial")
        </div>
    </div>
    <div class="container body-content">
        <br />
        @RenderBody()

        <br /><br />
        <nav class="navbar navbar-default navbar-fixed-bottom">
            <div style="text-align:center">
                <img src="~/Content/img/logo_small.png" class="img-responsive" />
                <sub style="position:absolute; right:10px; bottom:10px;">&copy; @DateTime.Now.Year </sub>
            </div>
        </nav>
    </div>

    @*---------- Javascripts ----------*@

    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/modernizr")
    @Scripts.Render("~/bundles/bootstrap")
    @Scripts.Render("~/Scripts/KnockOut/knockout-3.0.0.js")
    @Scripts.Render("~/Scripts/JQuery/jquery.unobtrusive-ajax.js")
    @Scripts.Render("~/Scripts/JQuery/jquery.validate.js")
    @Scripts.Render("~/Scripts/JQuery/jquery.validate.unobtrusive.js")
    @Scripts.Render("~/Scripts/JQuery/jquery.callout.unobtrusive.js")   
    @Scripts.Render("~/Scripts/MVCFoolProof/mvcfoolproof.unobtrusive.js")    
    @RenderSection("scripts",false)
</body>
</html>

Here is the code for the relevant partial that I am currently working on (there are a couple of similar partials that will be placed on the same page):

<form id="AddCrimeForm">
    <div class="panel panel-success">
        <div class="panel-heading">
            <div class="form-horizontal">
                <div class="row">
                    <div class="col-lg-11">Add a crime incident to the list</div>
                    <div class="col-lg-1">
                        <button type="submit" class="btn btn-success btn-xs" onclick="addCrime();"><i class="fa fa-plus"></i> Add</button>
                    </div>
                </div>
            </div>
        </div>

        <div class="panel-body">
            <div class="form-horizontal">
                <div class="row">
                    <div class="col-lg-6">
                        <input data-val="true" data-val-number="The field Id must be a number." data-val-required="The Id field is required." id="Id" name="Id" type="hidden" value="">
                        <div class="form-group">
                            <label class="control-label col-md-4" for="CaseNumber">Case Number</label>
                            <div class="col-md-8">
                                <input class="form-control text-box single-line" data-val="true" data-val-required="The Case Number field is required." id="CaseNumber" name="CaseNumber" type="text" value="">
                                <span class="field-validation-valid" data-valmsg-for="CaseNumber" data-valmsg-replace="true"></span>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="control-label col-md-4" for="DateOfIncident">Date Of Incident</label>
                            <div class="col-md-8">
                                <input class="form-control text-box single-line valid" data-val="true" data-val-required="The Date of Incident field is required." id="DateOfIncident" name="DateOfIncident" type="date" value="">
                                <span class="field-validation-valid" data-valmsg-for="DateOfIncident" data-valmsg-replace="true"></span>
                            </div>
                        </div>
                    </div>
                    <div class="col-lg-6">
                        <div class="form-group">
                            <label class="control-label col-md-4" for="Description">Description</label>
                            <div class="col-md-8">
                                <textarea class="form-control text-box multi-line" data-val="true" data-val-required="The Description field is required." id="Description" name="Description"></textarea>
                                <span class="field-validation-valid" data-valmsg-for="Description" data-valmsg-replace="true"></span>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</form>

<table class="table table-striped table-hover " id="CrimeList">
    <thead>
        <tr>
            <th>Case Number</th>
            <th>Date of Incident</th>
            <th>Description</th>
            <th></th>
        </tr>
    </thead>
    <tbody data-bind="foreach: items">
        <tr>
            <td data-bind="text: $data.CaseNumber">Column content</td>
            <td data-bind="text: $data.DateOfIncident">Column content</td>
            <td data-bind="text: $data.Description" style="text-wrap: normal">Column content</td>
            @*<td></td>
                <td></td>
                <td></td>*@
            <td>...</td>
        </tr>
    </tbody>
</table>

And here is the code for client_crime_kjs.js, with all my KnouckoutJS viewmodel code:

$(document).ready(
    function ()
    {

        var Crime = function(CaseNumber, DateOfIncident, Description)
        {
            this.CaseNumber = CaseNumber;
            this.DateOfIncident = DateOfIncident;
            this.Description = Description;
        }

        var initialData = new Array();

        var crimes = function (items)
        {
            var self = this;
            //Data
            self.items = ko.observableArray(items)

            //operations
            self.addCrime = function()
            {
                if ($("#AddCrimeForm").valid()) {
                    self.crime = new Crime($("#CaseNumber").val(), $("#DateOfIncident").val(), $("#Description").val());
                    //var JSONObj = { CaseNumber: $("#CaseNumber").val(), DateOfIncident: $("#DateOfIncident").val(), Description: $("#Description").val() };
                    self.items.push(this.crime);
                }

                //$("#CaseNumber").val() = "";
                //$("#DateOfIncident").val() = "";
                //$("#Description").val() = "";

            }

        }

        ko.applyBindings(crimes(initialData), $("#CrimeList")[0])
    }
);

Basically what happens is at this stage, is that when the fields are invalid, the form does not submit (rightly so), but when it does validate it does submit (contrary to my requirement), and my KO observablearray subsequently resets.

Was it helpful?

Solution

So I got the answer to the above question. The trick is to set the button type="button" in stead of "submit".

So, for anyone else struggling with this, here is an example of how to get it to work...

Your knockout ViewModel:

$(document).ready(
    function () {

        var Crime = function (CaseNumber, DateOfIncident, Description) {
            this.CaseNumber = CaseNumber;
            this.DateOfIncident = DateOfIncident;
            this.Description = Description;
        }

        var crimes = function (items) {
            var self = this;
            //Data
            self.items = ko.observableArray(items)

            //operations
            self.addCrime = function () {
                if ($("#AddCrimeForm").valid()) {
                    self.crime = new Crime($("#CaseNumber").val(), $("#DateOfIncident").val(), $("#Description").val());
                    //var JSONObj = { CaseNumber: $("#CaseNumber").val(), DateOfIncident: $("#DateOfIncident").val(), Description: $("#Description").val() };
                    self.items.push(this.crime);

                    $("#CaseNumber").val("");
                    $("#DateOfIncident").val("");
                    $("#Description").val("");
                }
            }

            self.removeCrime = function (item) {
                self.items().remove(item);
            }

        }

        var initialData = new Array();
        ko.applyBindings(crimes(initialData), $("#CrimeList")[0])
    }
);

And here is the corresponding HTML:

<form id="AddCrimeForm">
    <div class="panel panel-success">
        <div class="panel-heading">
            <div class="form-horizontal">
                <div class="row">
                    <div class="col-lg-11">Add a crime incident to the list</div>
                    <div class="col-lg-1">
                        <button type="button" class="btn btn-success btn-xs" onclick="addCrime();"><i class="fa fa-plus"></i> Add</button>
                    </div>
                </div>
            </div>
        </div>

        <div class="panel-body">
            <div class="form-horizontal">
                <div class="row">
                    <div class="col-lg-6">
                        <input data-val="true" data-val-number="The field Id must be a number." data-val-required="The Id field is required." id="Id" name="Id" type="hidden" value="">
                        <div class="form-group">
                            <label class="control-label col-md-4" for="CaseNumber">Case Number</label>
                            <div class="col-md-8">
                                <input class="form-control text-box single-line" data-val="true" data-val-required="The Case Number field is required." id="CaseNumber" name="CaseNumber" type="text" value="">
                                <span class="field-validation-valid" data-valmsg-for="CaseNumber" data-valmsg-replace="true"></span>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="control-label col-md-4" for="DateOfIncident">Date Of Incident</label>
                            <div class="col-md-8">
                                <input class="form-control text-box single-line valid" data-val="true" data-val-required="The Date of Incident field is required." id="DateOfIncident" name="DateOfIncident" type="date" value="">
                                <span class="field-validation-valid" data-valmsg-for="DateOfIncident" data-valmsg-replace="true"></span>
                            </div>
                        </div>
                    </div>
                    <div class="col-lg-6">
                        <div class="form-group">
                            <label class="control-label col-md-4" for="Description">Description</label>
                            <div class="col-md-8">
                                <textarea class="form-control text-box multi-line" data-val="true" data-val-required="The Description field is required." id="Description" name="Description"></textarea>
                                <span class="field-validation-valid" data-valmsg-for="Description" data-valmsg-replace="true"></span>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</form>

<table class="table table-striped table-hover " id="CrimeList">
    <thead>
        <tr>
            <th>Case Number</th>
            <th>Date of Incident</th>
            <th>Description</th>
            <th></th>
        </tr>
    </thead>
    <tbody data-bind="foreach: items">
        <tr>
            <td data-bind="text: $data.CaseNumber">Column content</td>
            <td data-bind="text: $data.DateOfIncident">Column content</td>
            <td data-bind="text: $data.Description" style="text-wrap: normal">Column content</td>
            @*<td></td>
                <td></td>
                <td></td>*@
            <td>...</td>
        </tr>
    </tbody>
</table>

Once again - take notice that the "Add" button's type has been set to "button" and NOT "submit".

Hope this helps the rest of all you coding peeps out there!

OTHER TIPS

I am in this moment trying to do something similar.

my idea at begining was same as yours, but then I made some changes because it is hard to do complex validation with data annotations. With complex I mean that a record do not repeat on a data base, or a custom format on an input.

So I went for FluentValidation, the problem is that fluent validation is not always working in unobtrusive validation. So what I am doing now is this.

hope this helps you and if you find something that could be better please let me know:

my model with its validation:

    public class BUCashFlow
{
    public int Id { get; set; }
    [Display(Name = "Concepto")]
    public string Text { get; set; }
    [Display(Name = "Valor")]
    public double Value { get; set; }
    public CashFlowType CashFlowType { get; set; }
    [Display(Name = "Cuenta")]
    public int AccountId { get; set; }
    public string User { get; set; }

    public virtual Account Account { get; set; }
}

public class BuCashFlowVal : AbstractValidator<BUCashFlow>
{
    public BuCashFlowVal()
    {

        RuleFor(p => p.Text)
            .NotEmpty().WithMessage(ValHelper.Messages.Required);
        RuleFor(p => p.Value)
            .NotEmpty().WithMessage(ValHelper.Messages.Required);
        RuleFor(p => p.AccountId)
            .NotEmpty().WithMessage(ValHelper.Messages.Required);
    }
}

I am using web api too, so here is my web api controller, where i Validate my new BUCashFlow model

        // POST api/BUCashFlows
    [ResponseType(typeof(BUCashFlow))]
    public IHttpActionResult PostBUCashFlow(BUCashFlow bucashflow)
    {
        ValidationResult ValRes = new BuCashFlowVal().Validate(bucashflow);
        if (!ValRes.IsValid)
        {
            return BadRequest(ValRes.Errors[0].ErrorMessage);
        }
        bucashflow.User = User.Identity.GetUserId();
        bucashflow.CashFlowType=CashFlowType.Purchase;
        db.BuCashFlows.Add(bucashflow);
        db.SaveChanges();

        return CreatedAtRoute("DefaultApi", new { id = bucashflow.Id }, bucashflow);
    }

finally to display my errors in js/ko i am doing this:

            self.addExpense = function(selector) {
            $.ajax({
                type: 'POST',
                url: '@ViewBag.ApiBUExpenses',
                data: $(selector).serialize()
            }).done(function(o) {
                self.expenses.push(new ExpenseVM(self, o.Id, o.Text, o.Value, o.AccountId));
            }).fail(function (o) {
                $(selector).find('.val').html(  '<div class="alert alert-warning alert-dismissable">' +
                                                    '<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>' +
                                                    '<strong>Warning!</strong> ' + o.responseJSON.Message +
                                                '</div>');
            });
        };

and my html form that was serialized in this ajax Call:

            <form id="new-expense-form" data-bind="submit: addExpense">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
                <h4 class="modal-title" id="myModalLabel">Agregar Nuevo Gasto</h4>
            </div>
            <div class="modal-body">
                <div class="form-group">
                    <label class="control-label">Concepto</label>
                    <input name="Text" type="text" class="form-control" />
                </div>
                <div class="form-group">
                    <label class="control-label">Valor</label>
                    <input name="Value" type="text" class="form-control" />
                </div>
                <div class="form-group">
                    <label class="control-label">Cuenta</label>
                    <i data-bind="visible: isLoadingAccounts" class="fa fa-refresh fa-spin pull-right"></i>
                    <select name="AccountId" class="form-control" data-bind="options: accounts, optionsText: 'name', optionsValue: 'Id', optionsCaption: 'Cuenta'"></select>
                </div>
                <div class="val"></div>
            </div>
            <div class="modal-footer">
                <button class="btn btn-default-pnl btn-circle-m" title="Guardar Gasto">
                    <i class="fa fa-check"></i>
                </button>
            </div>
        </form>

What you think about this approach?

If FoolProof works as espected

thanks for sharing this library, now if this works you just have to change the ajax call this way:

self.addExpense = function(selector) {
    $(selector).validate()
    if ($(selector).valid()) {
            $.ajax({
                type: 'POST',
                url: '@ViewBag.ApiBUExpenses',
                data: $(selector).serialize()
            }).done(function(o) {
                self.expenses.push(new ExpenseVM(self, o.Id, o.Text, o.Value, o.AccountId));
            })
    }
};    

Update

I saw your library and I think it wonrt work, if it doesnt, you always can duplicate your validtion and use KnockoutValidation

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top