Многоэтапная форма Knockout-JS с проверкой

Ищите здесь проверку на вменяемость. Недавно я начал изучать нокаут, и мне поручили преобразовать существующую многоступенчатую форму.

Основная идея заключается в проверке каждого шага, прежде чем позволить пользователю продолжить. Также установлены определенные ограничения (не показаны), которые определяют, следует ли продолжить работу или отправить с использованием всех текущих данных (например, если они не соответствуют требованиям).

Вот скрипка с упрощенной версией (фактическая форма содержит около 40 полей за 4 шага)

http://jsfiddle.net/dyngomite/BZcNg/

HTML:

<form id="register">
 <fieldset>
      <h2>About You</h2>
    <ul>
        <li>
            <label for="firstName">First Name:</label>
            <input type="text" data-bind="value: firstName" required="required" />
        </li>
        <li>
            <label for="lastName">Last Name</label>
            <input type="text" data-bind="value: lastName" required="required" />
        </li>
    </ul>
 </fieldset>
 <fieldset>
     <h2>Your Business</h2>

    <ul>
        <li>
            <label for="businessName">Business Name:</label>
            <input type="text" data-bind="value: businessName" required="required" />
        </li>
        <li>
            <label for="currentCustomer">Were you referred by someone?</label>
            <input type="checkbox" data-bind="checked: referred" />
        </li>
    </ul>
</fieldset>
<fieldset>
     <h2>User Info</h2>

    <ul>
        <li>
            <label for="userName">Referrer's First Name:</label>
            <input type="text" data-bind="value: referralFirst" required="required" />
        </li>
        <li>
            <label for="password">Referrer's Last Name:</label>
            <input type="password" data-bind="value: referralLast" required="required" />
        </li>
    </ul>
  </fieldset>
 </form>
<div class="nav-buttons"> <a href="#" data-bind='click: stepForward'>Continue</a>
    <a href="#" data-bind='click: stepBack'>Back</a>
    <a href="#" data-bind='click: resetAll'>Cancel</a>
 </div>

JS:

 $("#register").children().hide().first().show();

ko.validation.init({
   parseInputAttributes: true,
   decorateElement: true,
   writeInputAttributes: true,
   errorElementClass: "error"
});

function myViewModel() {

var self = this;

//observable init
self.firstName = ko.observable();
self.lastName = ko.observable();
self.businessName = ko.observable();
self.referred = ko.observable();
self.referralFirst = ko.observable();
self.referralLast = ko.observable();

//validaiton observable init
self.step1 = ko.validatedObservable({
    firstName: self.firstName,
    lastName: self.lastName
});

self.step2 = ko.validatedObservable({
    businessName: self.businessName,
    referred: self.referred
});

self.step3 = ko.validatedObservable({
    referralFirst: self.referralFirst,
    referralLast: self.referralLast
});

//navigation init
self.currentStep = ko.observable(1);

self.stepForward = function () {
    if(self.currentStep()<4){
        self.changeSection(self.currentStep() + 1);
    }
}

self.stepBack = function () {
    if (self.currentStep() > 1) {
        self.changeSection(self.currentStep() - 1);
    }
}

self.changeSection = function(destIdx){
    var validationObservable = "step" + self.currentStep();
    if(self[validationObservable]().isValid()){
        self.currentStep(destIdx);
        $("#register").children().hide().eq(self.currentStep() - 1).show();
        return true;
    }else{
        self[validationObservable]().errors.showAllMessages();
    }
    return false;
}

self.resetAll = function(){
    //TODO
    return false;
}

}

 ko.applyBindings(new myViewModel());

Мои вопросы:

  1. Имеет ли смысл объявить все поля изначально как наблюдаемые, а затем сгруппировать их вместе в validatedObservables()?

  2. Если в конце я хочу отправить всю форму, есть ли более разумный способ сделать это, чем объединение каждого шага с помощью ko.toJSON(self.step1()). Нужно ли мне создавать наблюдаемую «полную форму», содержащую все наблюдаемые входные данные? Другими словами, как лучше всего сериализовать полную форму? Хотел бы я использовать ko.toJSON(self) ?

  3. Каков наилучший способ сбросить форму до первоначальной конфигурации? Есть ли способ повторно применить ko.applyBindings(new myViewModel())?

Я правильно об этом говорю?

Спасибо за любые разъяснения.


person lyma    schedule 23.05.2013    source источник


Ответы (2)


Это хорошее начало. Я предлагаю вам управлять видимостью с помощью нокаута и обращаться к jQuery только тогда, когда нет другого выхода. Под этим я подразумеваю управление видимостью наборов полей:

<fieldset data-bind="visible: currentStep() === 1">
  1. Да, имеет смысл изначально иметь все поля в качестве наблюдаемых. Хорошая стратегия состоит в том, чтобы получить ваши данные в виде JSON с сервера и использовать плагин сопоставления, чтобы преобразовать все в наблюдаемые. Если вы предпочитаете кодировать все вручную, это нормально.

  2. В конце просто отправьте всю модель представления: ko.toJSON(self) выполнит сериализацию ее в JSON. Вы можете преобразовать его в объект JS: ko.toJS, затем очистить данные, которые вы не хотите отправлять (например, данные поиска и т. д.), а затем использовать JSON.stringify для преобразования в JSON.

  3. Трудно сбросить состояние проверки с помощью плагина проверки. Чтобы сбросить форму, просто удалите существующую форму из DOM и примените привязки к новому HTML. Держите HTML где-нибудь под рукой на странице:

Чтобы сбросить форму, выполните:

<script type="text/html" id="ko-template">
   <form id="register"> 
   ...
   </form>
</script>

<div id="context"></div>

JavaScript:

var template = $('#ko-template').html();

$('#context').empty().html(template);

ko.applyBindings(new myViewModel(), document.getElementById('context'));

Тег формы в этом случае не нужен, так как вы управляете всем с помощью JS-объектов.

person Tomas Kirda    schedule 24.05.2013
comment
Спасибо. Теперь я вижу, что привязка видимости к currentStep() является более чистым подходом. 1. Я планирую интегрировать картографический плагин после воссоздания всех базовых функций. 2. Я думаю, вы правы, что мне нужно будет использовать ko.toJS(), а затем ограничить передаваемые поля. 3. Интересно, а обязательно ли использовать КО-шаблон? В этом случае у меня будет разметка формы внутри тега script, а также внутри div#context. Было бы плохой идеей отказаться от шаблона и просто сохранить HTML-код формы внутри глобальной переменной var на готовом документе, а затем выполнить остальную часть вашей процедуры? - person lyma; 25.05.2013

Взгляните на ValidatedViewModel от Карла Шрёдла.

При использовании в сочетании с отличным плагином Knockout Validation вы можете создавать группы ограничений проверки и применять их как требуется.

При каждом запуске вашей процедуры проверки вы будете удалять все группы ограничений, а затем применять группы ограничений, которые вы хотите для данного шага. В качестве альтернативы подпишитесь на наблюдаемый шаг, чтобы установить группы ограничений.

(Я предлагаю использовать оператор try/catch при применении/удалении групп ограничений, поскольку это приведет к ошибке, если группа ограничений уже была применена/удалена.)

К этому прилагается некоторая кривая обучения, но это действительно помогло мне создать страницу корзины/оформления заказа с соответствующей проверкой на каждом этапе.

Обновление: здесь представлен обновленный jsfiddle с использованием ValidatedViewModel. Я сделал видимый шаг зависимым от наблюдаемого currentStep и удалил необходимые теги. Вся проверка теперь выполняется в модели. В качестве бонуса CSS в jsfiddle также оформляет сообщение проверки без дополнительной разметки.

ko.validation.init({
    parseInputAttributes: false,
    decorateElement: true,
    insertMessages: true,
    messagesOnModified: true,
    grouping: { deep: true, observable: true }
});

var myViewModel = ValidatedViewModel(function () {
    var self = this;

    //observable init
    self.firstName = ko.observable();
    self.lastName = ko.observable();
    self.businessName = ko.observable();
    self.referred = ko.observable();
    self.referralFirst = ko.observable();
    self.referralLast = ko.observable();

    //navigation init
    self.currentStep = ko.observable(1);

    self.stepForward = function () {
        if(self.currentStep()<4){
            self.changeSection(self.currentStep() + 1);
        }
    }

    self.stepBack = function () {
        if (self.currentStep() > 1) {
            self.changeSection(self.currentStep() - 1);
        }
    }

    self.changeSection = function(destIdx){
        //remove all constraint groups
        try { self.removeConstraintGroup('step1'); } catch (e) { }
        try { self.removeConstraintGroup('step2'); } catch (e) { }
        try { self.removeConstraintGroup('step3'); } catch (e) { }

        //apply constraint group for current step
        try{self.applyConstraintGroup('step' + self.currentStep());} catch(e){}

        var errorCount = self.errors().length;

        self.errors.showAllMessages();
        if(errorCount===0){
            self.currentStep(destIdx);
            return true;
        }
        return false;
    }


    self.constraintGroups = {
        step1: {
            firstName: { required: true },
            lastName: { required: true }
        },
        step2: {
            businessName: { required: true }
        },
        step3: {
            referralFirst: { required: true },
            referralLast: { required: true }
        }

    }

    self.resetAll = function(){
        //TODO
        return false;
    }

    this.errors = ko.validation.group(this);

});

ko.applyBindings(new myViewModel());

Теперь HTML выглядит так:

<form id="register">
    <h1>Current Step: <span data-bind="text:currentStep()"></span></h1>
    <fieldset data-bind="visible: currentStep()===1">
         <h2>About You</h2>

        <ul>
            <li>
                <label for="firstName">First Name:</label>
                <input type="text" data-bind="value: firstName"  />
            </li>
            <li>
                <label for="lastName">Last Name</label>
                <input type="text" data-bind="value: lastName"  />
            </li>
        </ul>
    </fieldset>
    <fieldset data-bind="visible:currentStep()===2">
         <h2>Your Business</h2>

        <ul>
            <li>
                <label for="businessName">Business Name:</label>
                <input type="text" data-bind="value: businessName"  />
            </li>
            <li>
                <label for="currentCustomer">Were you referred by someone?</label>
                <input type="checkbox" data-bind="checked: referred" />
            </li>
        </ul>
    </fieldset>
    <fieldset data-bind="visible:currentStep()===3">
         <h2>User Info</h2>

        <ul>
            <li>
                <label for="userName">Referrer's First Name:</label>
                <input type="text" data-bind="value: referralFirst"  />
            </li>
            <li>
                <label for="password">Referrer's Last Name:</label>
                <input type="password" data-bind="value: referralLast"  />
            </li>
        </ul>
    </fieldset>
</form>
<div class="nav-buttons"> <a href="#" data-bind='click: stepForward'>Continue</a>
 <a href="#" data-bind='click: stepBack'>Back</a>
 <a href="#" data-bind='click: resetAll'>Cancel</a>

</div>
person 79IT    schedule 24.05.2013
comment
Это интересный подход, но я не вижу преимуществ использования validatedViewModel по сравнению с кластеризацией отдельных шагов в validatedObservable и последующим вызовом метода isValid(). Конечный результат кажется таким же, без необходимости try/catch, не так ли? - person lyma; 25.05.2013
comment
Кроме того, я устанавливаю parseInputAttributes = true и сохраняю свои ограничения проверки встроенными, чтобы использовать собственное поведение браузера, когда оно доступно, но это личное предпочтение. - person lyma; 25.05.2013
comment
Этот подход основан на масштабируемости. Ваш первоначальный подход будет работать нормально, если модель небольшая, а ваши требования к проверке относительно простые. Подумайте, что происходит, когда вам нужны разные параметры проверки, такие как поля пароля/подтверждения пароля. Как вы будете реализовывать этот синтаксический анализ атрибутов? Поле пароля должно содержать не менее 8 символов, а пароль и пароль подтверждения должны совпадать. Вы также хотите отобразить соответствующее сообщение проверки. С группами ограничений это чисто, аккуратно и отдельно от представления, но без этого становится очень сложно. - person 79IT; 25.05.2013
comment
Идея довольно хороша, хотя я не понимаю, зачем создавать функцию для обработки шагов, поскольку сейчас вы в основном жестко кодируете шаги. Почему бы не подписаться на currentStep(), а затем изменить вещи в соответствии с newValue? - person Nickvda; 26.08.2014