Knockout: Переключаем проверку ввода на русский язык или Knockout.Validation Localize (Globalize)

ru-RU | создано: 04.03.2013 | опубликовано: 04.03.2013 | обновлено: 01.01.2018 | просмотров за всё время: 9248

Если вы используете Knockoutjs, то наверное уже не раз приходилось делать проверку данных, которые вводит пользователь. А как вы проверяли ввод даты и дробных чисел? В этот статье настроим валидацию Knockout.Validations на работу "по-русски".

Подготовим проект к экспериментам

Создадим новый проект для теста. Обновим установленные пакеты, и установим пару-тройку новых. Первый пакет будет jssite, он должен быть вам уже знаком. После него установим еще один пакет kolite, о котором чуть позже. Установим Knockout.Validations (надо его тоже включить в набор скриптов пакета jssite).  А Knockoutjs уже установился вместе с пакетом jssite.

А еще я удалил сразу же файл BundleConfig.cs и всё что с ним связано. Причины можно почитать в другой статье.

Так как я использовал для создания нового проекта MvcApplication шаблон Basic (появляется после установки ASP.NET and Web Tools 2012.2 Released), то мой шаблон выглядит так _Layout.cshtml:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <link href="~/Content/all.min.css" rel="stylesheet" />
    <script src="~/Scripts/modernizr-2.6.2.min.js"></script>
</head>
<body>
    @RenderBody()
    <script src="~/Scripts/jquery-1.9.1.min.js"></script>

    <script src="~/Scripts/amplify.min.js"></script>
    <script src="~/Scripts/underscore.min.js"></script>
    <script src="~/Scripts/moment.min.js"></script>
    <script src="~/Scripts/toastr.min.js"></script>

    <script src="~/Scripts/knockout-2.2.1.js"></script>
    <script src="~/Scripts/knockout.validation.js"></script>
    <script src="~/Scripts/knockout.mapping-latest.js"></script>
    <script src="~/Scripts/knockout.dirtyFlag.js"></script>
    <script src="~/Scripts/knockout.command.js"></script>
    <script src="~/Scripts/knockout.activity.js"></script>

    <script src="~/Scripts/app/site.bindingHandlers.js"></script>
    <script src="~/Scripts/app/site.core.js"></script>
    <script src="~/Scripts/app/site.services.js"></script>
    <script src="~/Scripts/app/site.controls.js"></script>

    @RenderSection("scripts", required: false)
</body>
</html>

Контролер

По умолчанию в моем шаблоне нет контролера, хотя в маршрутах (RouteConfog.cs) прописан “Home”, его-то я и создал. А также создал представление (View) для одного единственного метода этого контролера. Пока оно вот такое:

@{
    ViewBag.Title = "Index";
 }
 <h2>Index</h2>

JavaScript ViewModel’ы

Теперь создаем сущность для теста (person), это тот объект, который мы будем проверять на правильность ввода на форме:

function Person(last, first, second, birth, weight) {
    return {
        lastName: ko.observable(last),
        firstName: ko.observable(first),
        secondName: ko.observable(second),
        birthDate: ko.observable(birth),
        weight: ko.observable(weight)
    };
}

Теперь создаем ViewModel для страницы Index.cshtml:

$(function (parameters) {

    "use strict";

    site.vm.viewModel = function () {
        var
            data = [
                new Person("Суходрищев", "Дормидонт", "Евлампиевич", "03/03/1983", "92.3"),
                new Person("Тихобздеев", "Гавриил", "Афанасьевич", "13.03.1976", "90,8")
            ],
            
            meta = new site.controls.Metadata(
                "Тестирование валидации Knockout",
                "Проверяем как работает валидация Knockout,\
                в том числе локализацию Knockout на русский язык: цифры, даты и т.д.",
                "http://www.calabonga.net"),
            clock = new site.controls.Clock(),
            person = data[0],
            person2 = data[1],
            errors = ko.validatedObservable(person),
            errors2 = ko.validatedObservable(person2),
            saveCommand = ko.asyncCommand({
                execute: function (complete) {
                    alert("Saved!");
                    complete();
                },
                canExecute: function (isExecuting) {
                    return !isExecuting && errors.isValid;
                }
            }),
            saveCommand2 = ko.asyncCommand({
                execute: function (complete) {
                    alert("Saved 2!");
                    complete();
                },
                canExecute: function (isExecuting) {
                    return !isExecuting && errors2.isValid;
                }
            });

        return {
            meta: meta,
            clock: clock,
            person: person,
            person2: person2,
            saveCommand: saveCommand,
            saveCommand2: saveCommand2
        };
    }();

    ko.applyBindings(site.vm.viewModel);
});

Пояснения…

Строка 3: включаем “строгость”.

Строки 7-10: создаем два объекта Person, один валидный для EN локализации, другой валиден для RU валидации (смотреть надо на дату и вес).

Строки 12-16: метаданные для главной странице, кстати, класс метаданных “переехал” в jssite в namespace “controls”. В строке 17: создаем, как уже повелось, объект Clock. Если при первом старте часы пойдут – скрипты установлены правильно!

Строки 17-18: создаем две переменных для хранение на форме двух Person.

Строки 22-30 и строки 31-39: создаем asyncCommand (как раз из того самого пакета kolite) для более полной реализации паттерна MVVM на форме.

Строки 41-48: Выставляем публичные свойства.

Строка 51: Привязываем данные к форме.

Форма (представление)

Форма больше не будет меняться, поэтому приведу ее целиком:

@{
    ViewBag.Title = "Index";
}
<h2>Index</h2>


<div data-bind="text: clock.time" id="clock"></div>
<h2 data-bind="text: meta.title"></h2>
<p data-bind="text: meta.description"></p>

<table>
    <thead>
        <tr>
            <th style="width:50%">Globalization EN</th>
            <th style="width:50%">Globalization RU</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td data-bind="css: { 'valid': person.isValid(), 'invalid': !person.isValid() }">
                <p><span data-bind="text: person.isValid()"></span></p>
                <div data-bind="with: person">
                    <div class="editor-label">
                        <label>Фамилия:</label>
                    </div>
                    <div class="editor-field">
                        <input type="text" data-bind="value: lastName" />
                    </div>

                    <div class="editor-label">
                        <label>Имя:</label>
                    </div>
                    <div class="editor-field">
                        <input type="text" data-bind="value: firstName" />
                    </div>

                    <div class="editor-label">
                        <label>Отчество:</label>
                    </div>
                    <div class="editor-field">
                        <input type="text" data-bind="value: secondName" />
                    </div>

                    <div class="editor-label">
                        <label>Дата рождения:</label>
                    </div>
                    <div class="editor-field">
                        <input type="text" data-bind="value: birthDate" />
                    </div>

                    <div class="editor-label">
                        <label>Вес:</label>
                    </div>
                    <div class="editor-field">
                        <input type="text" data-bind="value: weight" />
                    </div>

                </div>
                <p>
                    <button data-bind="command: saveCommand, text: 'сохранить'"></button>
                </p>
            </td>
            <td data-bind="css: { 'valid': person2.isValid(), 'invalid': !person2.isValid() }">
                <p><span data-bind="text: person2.isValid()"></span></p>
                <div data-bind="with: person2">
                    <div class="editor-label">
                        <label>Фамилия:</label>
                    </div>
                    <div class="editor-field">
                        <input type="text" data-bind="value: lastName" />
                    </div>

                    <div class="editor-label">
                        <label>Имя:</label>
                    </div>
                    <div class="editor-field">
                        <input type="text" data-bind="value: firstName" />
                    </div>

                    <div class="editor-label">
                        <label>Отчество:</label>
                    </div>
                    <div class="editor-field">
                        <input type="text" data-bind="value: secondName" />
                    </div>

                    <div class="editor-label">
                        <label>Дата рождения:</label>
                    </div>
                    <div class="editor-field">
                        <input type="text" data-bind="value: birthDate" />
                    </div>

                    <div class="editor-label">
                        <label>Вес:</label>
                    </div>
                    <div class="editor-field">
                        <input type="text" data-bind="value: weight" />
                    </div>

                </div>
                <p>
                    <button data-bind="command: saveCommand2, text: 'сохранить'"></button>
                </p>
            </td>
        </tr>
    </tbody>
</table>

@section scripts
{
    <script src="~/Scripts/app/site.vm.viewModel.js"></script>
}

А вот как она выглядит:

113-1
(рис 1. Перед установкой локализации)

Время проверять

Но для начала руссифицируем валидацию у knockout. Для этого находим на сайте Knockout-Validation папку с названием Localization, там уже есть ru-RU.js, вот его-то и скачаем чтобы добавить в проект. Таким образом, мы включаем перевод на русский язык валидацию примитивных типов.

Теперь надо подключить локализацию, я это сделаю в файле site.core.js, там у меня находится bootstrapper и всё остальное. Вот измененный файл:

// base namespace
var site = site || {};

// config module
site.cfg = site.cfg || {};

// model's module 
site.m = site.m || {};

// viewmodel's module
site.vm = site.vm || {};

// services module
site.services = site.services || {};

// utilites module
site.utils = site.utils || {};

// controls module
site.controls = site.controls || {};

// start engine
var bootstrapper = function () {

    var root = this,
        initLibs = function () {

            // initialization for third-party libs
            site.amplify = root.amplify;
            site.$ = root.jQuery;
            site.logger = root.toastr;
            site._ = root._;
        },
        initValidation = function () {
            ko.validation.configure({
                registerExtenders: true,    //default is true
                messagesOnModified: true,   //default is true
                insertMessages: true,       //default is true
                parseInputAttributes: true, //default is false
                writeInputAttributes: true, //default is false
                messageTemplate: null,      //default is null
                decorateElement: true       //default is false. Applies the 
                                              validationElement CSS class
            });
            ko.validation.localize({
                required: 'Необходимо заполнить это поле.',
                min: 'Значение должно быть больше или равно {0}.',
                max: 'Значение должно быть меньше или равно {0}.',
                minLength: 'Длина поля должна быть не меньше {0} символов.',
                maxLength: 'Длина поля должна быть не больше {0} символов.',
                pattern: 'Пожалуйста проверьте это поле.',
                step: 'Значение поле должно изменяться с шагом {0}',
                email: 'Введите в поле правильный адрес email',
                date: 'Пожалуйста введите правильную дату',
                dateISO: 'Пожалуйста введите правильную дату в формате ISO',
                number: 'Поле должно содержать число',
                digit: 'Поле должно содержать цифры',
                phoneUS: 'Поле должно содержать правильный номер телефона',
                equal: 'Значения должны быть равны',
                notEqual: 'Пожалуйста выберите другое значение.',
                unique: 'Значение должно быть уникальным.'
            });
        },
        initConfig = function () {

            // settings for site
            site.cfg.throttle = 600;
            site.cfg.busyIndicatorImageName = "/images/ms-loader.gif";

            // pager 
            site.cfg.pageSize = 10;
            site.cfg.groupSize = 10;
            site.cfg.pageSizes = ko.observableArray([5, 10, 20, 30, 50, 100]);
        },
        init = function () {
            initLibs();
            initConfig();
            initValidation();
        };

    return {
        run: init
    };

}();

Строка 34-61: Настройка и локализация knockout.Validation, которую вызываем в методе инициализации в строке 77.

Запустив проект, я всё также вижу ту же картинку – EN – зеленый, RU – красный. Значит валидация еще не настроена.

Ключевой момент Localization или Globalize.culture

К счастью, или к сожалению, но всё уже давно придумали за нас. Уже существует сборка (если так можно выразиться для js-файла), которая реализует весь требуемый для нас функционал - globalize. На сайте хорошо расписано для чего предназначена сборка. Подходит она и для нашего случая. Качаем… добавляем в проект:


113-2
(рис. 2. Подключение глобализации)

Не забудьте прописать скрипты в шаблоне _Layout.cshtml, чтобы они загружались на всех страницах приложения, или там, где вам будет угодно. Я добавил в шаблон, для простоты.

Теперь в файле site.core.js подключаем “глобализацию”. В методе InitLibs (см. строки 26-33 предыдущего листинга) придется дописать некоторое количество строк:


initLibs = function () {

    //globalize
    Globalize.culture("ru");

    // подменяем парсинг чисел с плавающей точкой
    // потому что в Globalize.parseFloat существует бага,
    // так как "точка" (.) обрабатывается как часть числа
    // даже если она таковой не является.
    // Я исправил это так:
            
    Globalize.orgParaseFloat = Globalize.parseFloat;
    Globalize.parseFloat = function (value) {
        value = String(value);
        var culture = this.findClosestCulture();
        var seperatorFound = false;
        for (var i in culture.numberFormat) {
            if (culture.numberFormat[i] == ".") {
                seperatorFound = true; break;
            }
        }
        if (!seperatorFound) { value = value.replace(".", "NaN"); }
        return this.orgParaseFloat(value);
    };

    // устанавливаем собственные валидаторы
    // knockout для "number" и для "data"
            
    ko.validation.rules.number.validator = function (value, validate) {
        return !(value) || (validate && !isNaN(Globalize.parseFloat(value)));
    };

    ko.validation.rules.date.validator = function (value, validate) {
        return !(value) || (validate && Globalize.parseDate(value) != null);
    };

    // initialization for third-party libs
    site.amplify = root.amplify;
    site.$ = root.jQuery;
    site.logger = root.toastr;
    site._ = root._;
}

Строка 4: подключает локализацию к javascript.

Обратите внимание на то, что в Globalize.parseFloat существует некоторая оказия, дело в том, что “точка” (.) принимается библиотекой как часть числа, даже если она не является его частью. Строки 12-14 исправляют эту оплошность.

Так же, надо не только подключить Globalize, исправив багу, но еще и немного переопределить правила проверки валидности у самого knockoutjs. И здесь уже придется написать код самостоятельно. Я написал проверку для DateTime и Number:

ko.validation.rules.number.validator = function (value, validate) {
    return !(value) || (validate && !isNaN(Globalize.parseFloat(value)));
};

ko.validation.rules.date.validator = function (value, validate) {
    return !(value) || (validate && Globalize.parseDate(value) != null);
};

Других правил мне пока не требуется… Запускаем! УРА!!!

113-3
(рис. 3. Knockout.Validation “по-русски”)

То что и требовалось!