0

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.

2 Answers2

2

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!

0

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

bto.rdz
  • 6,636
  • 4
  • 35
  • 52
  • For me the complexity of the validations is not a problem - I use Foolproof for my DataAnnotations validations and for the edge cases where even more complex validation is required, I simply do validation on server-side only (client is quite happy for me to do it this way). –  Feb 22 '14 at 18:27
  • If I understand your code correctly, you don't do any client-side validation but rather send through an AJAX request to the server for validation? In my case this would not suffice as we need to architect for offline functionality (in other words, user must be able to capture all data even if the connection is offline, without validation breaking). I appreciate the effort though - I believe that, were it not for this requirement, this would have worked. So +1. –  Feb 22 '14 at 18:30
  • Yes, but if FoolProof works all you have to change is 2 or 3 lines I will update, and use this library too if ths works! – bto.rdz Feb 22 '14 at 18:31