Knockout.js向导验证每个步骤

问题描述:

我已经设法根据Niemeyer给出的答案创建了一个简单的向导。这工作正常。我想添加验证。我已经设法在字段Firstname上添加必需的验证字。将此项留空将显示错误。但是我不能成功的是: 验证当前步骤中的模型,并根据是否存在错误启用或禁用该模型。如果启用或禁用下一个按钮太困难,那没问题。出现错误时,我也可以在没有禁用按钮的情况下生活。只要用户无法在发生错误时继续下一步。Knockout.js向导验证每个步骤

。我的看法是这样的:

//model is retrieved from server model 
<script type="text/javascript"> 
    var serverViewModel = @Html.Raw(Json.Encode(Model)); 
</script> 


<h2>Test with wizard using Knockout.js</h2> 
    <div data-bind="template: { name: 'currentTmpl', data: currentStep }"></div> 
<hr/> 

<button data-bind="click: goPrevious, enable: canGoPrevious">Previous</button> 
<button data-bind="click: goNext, enable: canGoNext">Next</button> 

<script id="currentTmpl" type="text/html"> 
    <h2 data-bind="text: name"></h2> 
    <div data-bind="template: { name: getTemplate, data: model }"></div> 
</script> 

<script id="nameTmpl" type="text/html"> 
    <fieldset> 
     <legend>Naamgegevens</legend> 
     <p data-bind="css: { error: FirstName.hasError }"> 
      @Html.LabelFor(model => model.FirstName) 
      @Html.TextBoxFor(model => model.FirstName, new { data_bind = "value: FirstName, valueUpdate: 'afterkeydown'"}) 
      <span data-bind='visible: FirstName.hasError, text: FirstName.validationMessage'> </span> 
     </p> 
     @Html.LabelFor(model => model.LastName) 
     @Html.TextBoxFor(model => model.LastName, new { data_bind = "value: LastName" }) 
    </fieldset> 
</script> 

<script id="addressTmpl" type="text/html"> 
    <fieldset> 
     <legend>Adresgegevens</legend> 
     @Html.LabelFor(model => model.Address) 
     @Html.TextBoxFor(model => model.Address, new { data_bind = "value: Address" }) 
     @Html.LabelFor(model => model.PostalCode) 
     @Html.TextBoxFor(model => model.PostalCode, new { data_bind = "value: PostalCode" }) 
     @Html.LabelFor(model => model.City) 
     @Html.TextBoxFor(model => model.City, new { data_bind = "value: City" }) 
    </fieldset> 
</script> 

<script id="confirmTmpl" type="text/html"> 
     <fieldset> 
     <legend>Naamgegevens</legend> 
     @Html.LabelFor(model => model.FirstName) 
     <b><span data-bind="text:NameModel.FirstName"></span></b> 
     <br/> 
     @Html.LabelFor(model => model.LastName) 
     <b><span data-bind="text:NameModel.LastName"></span></b> 
    </fieldset> 
    <fieldset> 
     <legend>Adresgegevens</legend> 
     @Html.LabelFor(model => model.Address) 
     <b><span data-bind="text:AddressModel.Address"></span></b> 
     <br/> 
     @Html.LabelFor(model => model.PostalCode) 
     <b><span data-bind="text:AddressModel.PostalCode"></span></b> 
     <br/> 
     @Html.LabelFor(model => model.City) 
     <b><span data-bind="text:AddressModel.City"></span></b>   
    </fieldset> 
    <button data-bind="click: confirm">Confirm</button> 
</script> 

<script type='text/javascript'> 
    $(function() { 
     if (typeof(ViewModel) != "undefined") { 
      ko.applyBindings(new ViewModel(serverViewModel)); 
     } else { 
      alert("Wizard not defined!"); 
     } 
    }); 
</script> 

的knockout.js实施看起来是这样的:

function Step(id, name, template, model) { 
    var self = this; 
    self.id = id; 
    self.name = ko.observable(name); 
    self.template = template; 
    self.model = ko.observable(model); 

    self.getTemplate = function() { 
     return self.template; 
    }; 
} 

function ViewModel(model) { 
    var self = this; 

    self.nameModel = new NameModel(model); 
    self.addressModel = new AddressModel(model); 

    self.stepModels = ko.observableArray([ 
      new Step(1, "Step1", "nameTmpl", self.nameModel), 
      new Step(2, "Step2", "addressTmpl", self.addressModel), 
      new Step(3, "Confirmation", "confirmTmpl", {NameModel: self.nameModel, AddressModel:self.addressModel})]); 

    self.currentStep = ko.observable(self.stepModels()[0]); 

    self.currentIndex = ko.dependentObservable(function() { 
     return self.stepModels.indexOf(self.currentStep()); 
    }); 

    self.getTemplate = function(data) { 
     return self.currentStep().template(); 
    }; 

    self.canGoNext = ko.dependentObservable(function() { 
     return self.currentIndex() < self.stepModels().length - 1; 
    }); 

    self.goNext = function() { 
     if (self.canGoNext()) { 
      self.currentStep(self.stepModels()[self.currentIndex() + 1]); 
     } 
    }; 

    self.canGoPrevious = ko.dependentObservable(function() { 
     return self.currentIndex() > 0; 
    }); 

    self.goPrevious = function() { 
     if (self.canGoPrevious()) { 
      self.currentStep(self.stepModels()[self.currentIndex() - 1]); 
     } 
    }; 
} 

NameModel = function (model) { 

    var self = this; 

    //Observables 
    self.FirstName = ko.observable(model.FirstName).extend({ required: "Please enter a first name" });; 
    self.LastName = ko.observable(model.LastName); 

    return self; 
}; 

AddressModel = function(model) { 

    var self = this; 

    //Observables 
    self.Address = ko.observable(model.Address); 
    self.PostalCode = ko.observable(model.PostalCode); 
    self.City = ko.observable(model.City); 

    return self; 
}; 

而且我已经添加了必要的验证的扩展在该领域使用的名字:

ko.extenders.required = function(target, overrideMessage) { 
    //add some sub-observables to our observable  
    target.hasError = ko.observable(); 
    target.validationMessage = ko.observable(); 
    //define a function to do validation  

    function validate(newValue) { 
     target.hasError(newValue ? false : true); 
     target.validationMessage(newValue ? "" : overrideMessage || "This field is required"); 
    } 

    //initial validation  
    validate(target()); 

    //validate whenever the value changes  
    target.subscribe(validate); 
    //return the original observable  
    return target; 
}; 

这是一个棘手的问题,但我会为您提供一些解决方案...

如果您只是想阻止Next按钮继续使用无效的模型状态,那么我发现的最简单的解决方案是,从用于显示验证消息的<span>标签中的每一个添加一个类开始:

<span class="validationMessage" 
     data-bind='visible: FirstName.hasError, text: FirstName.validationMessage'> 

(奇数格式化以防止水平滚动)

接着,在goNext功能,改变代码以包括用于任何验证消息是否是可见的检查,例如:

self.goNext = function() { 
    if (
     (self.currentIndex() < self.stepModels().length - 1) 
     && 
     ($('.validationMessage:visible').length <= 0) 
     ) 
    { 
     self.currentStep(self.stepModels()[self.currentIndex() + 1]); 
    } 
}; 

现在,您可能会问“为什么不把该功能放在canGoNext依赖可观察?”中,并且答案是调用该函数并不像它可能会发生的那样工作。

因为canGoNextdependentObservable,所以它的值是在它是变化成员的模型的任何时候计算的。但是,如果它的模型没有改变,CanGoNext只是返回最后计算的值,即模型没有改变,那么为什么要重新计算它呢?

当仅检查是否还有更多步骤时,这并不重要,但是当我尝试在该函数中包含验证时,这起到了作用。

为什么?那么,例如,更改First Name会更新它所属的NameModel,但在ViewModel中,self.nameModel未设置为可观察对象,因此尽管NameModel发生更改,但self.nameModel仍然相同。因此,ViewModel没有改变,所以没有理由重新计算canGoNext。最终的结果是,CanGoNext始终将该表单看作有效的,因为它总是检查self.nameModel,它永远不会改变。

令人困惑,我知道,所以让我再向你扔更多的代码......

这里是ViewModel的开始,我结束了:

function ViewModel(model) { 
    var self = this; 

    self.nameModel = ko.observable(new NameModel(model)); 
    self.addressModel = ko.observable(new AddressModel(model)); 

    ... 

正如我提到的,模型需要可观察到知道发生了什么给他们。

现在更改goNextgoPrevious方法将工作而无需对这些模型观察到,但要获得你正在寻找的真正的实时验证,当按钮被禁用时,形式是无效的,使得模型可观察是必要的。

尽管我最终保留了canGoNextcanGoPrevious函数,但我没有将它们用于验证。我会稍微解释一下。

不过,首先,这是我加入ViewModel用于验证的功能:

self.modelIsValid = ko.computed(function() { 
    var isOK = true; 
    var theCurrentIndex = self.currentIndex(); 
    switch(theCurrentIndex) 
    { 
     case 0: 
      isOK = (!self.nameModel().FirstName.hasError() 
        && !self.nameModel().LastName.hasError()); 
      break; 
     case 1: 
      isOK = (!self.addressModel().Address.hasError() 
        && !self.addressModel().PostalCode.hasError() 
        && !self.addressModel().City.hasError()); 
      break; 
     default: 
      break; 
    }; 
    return isOK;     
}); 

[是啊,我知道...这个功能情侣视图模型到NameModel和AddressModel类甚至不是简单地引用更多每个这些类的实例,但是现在,就这样吧]

这里就是我如何绑定在HTML此项功能:

<button data-bind="click: goPrevious, 
        visible: canGoPrevious, 
        enable: modelIsValid">Previous</button> 
<button data-bind="click: goNext, 
        visible: canGoNext, 
        enable: modelIsValid">Next</button> 

请注意,我更改了canGoNextcanGoPrevious,因此每个都绑定到其按钮的visible属性,并且我将modelIsValid函数绑定到enable属性。

canGoNextcanGoPrevious函数就像您提供的那样 - 没有变化。

这些绑定更改的一个结果是Previous按钮在Name步骤中不可见,Next按钮在Confirm步骤中不可见。

此外,在对所有数据属性及其关联表单域进行验证时,从任何字段中删除值将立即禁用“下一个”和/或“上一个”按钮。

哎哟,这是很多解释!

我可能已经离开的东西了,但这里的链接,我以前得到这个工作的小提琴:http://jsfiddle.net/jimmym715/MK39r/

我敢肯定,有更多的工作要做,更多的障碍跨越大功告成之前与此,但希望这个答案和解释帮助。

+2

我可能会做得非常类似,但可能会把“Step”对象的通用计算看起来是否有任何模型属性无效(当前代码只会执行顶级道具)。我会避免将视图模型绑定到视图上,方法是查找具有特定类的元素,并在模型中保存逻辑'$('。validationMessage:visible')'。 http://jsfiddle.net/rniemeyer/MK39r/23/ – 2012-07-29 20:36:38

+0

我喜欢这个解决方案!我曾想过在每个模型类中都使用'modelIsValid',但我认为这同样糟糕,甚至比我最终得到的还要糟糕。虽然我没有想过在Step中定义它。我之前也没有像JS那样做过反射。伟大的东西......永远感谢你的投入,瑞恩。 – jimmym715 2012-07-29 20:45:06

+0

我已经通过使用knockout.validation解决了它。在我看来,我认为这比查看元素是否可见更清晰。我会将你的答复标记为答案。如果有人有兴趣,我可以稍后发布使用knockout.validation的解决方案。 – Mounhim 2012-07-30 19:56:39