Validation de données et custom bindings avec #KnockoutJS

Quand il s’agit d’insérer des éléments dans une liste personnalisée, SharePoint propose une interface certes efficace mais qui peut manquer d’ergonomie.

Prenons comme exemple la création de session pour un événement. Pour un petit nombre de session, l’interface classique reste convenable. Pour un plus grand nombre, la vue Excel vient à la rescousse mais… le format des dates et certaines contraintes fonctionnelles peuvent poser problème. Alors on se met à imaginer une interface plus intuitive avec l’ajout de session dynamique, un datepicker pratique, de la validation de champs.

KnockoutJS couplé avec le JSOM de SharePoint me semble répondre aux critères de la demande et me permet d’éviter un voyage dans le passé (WebForm, Viewstate). Pour la validation, Knockout-Validation propose un système extensible comprenant les cas les plus classique (requis, regex, …). Pour le datepicker, j’ai trouvé DateTimePicker qui permet de sélectionner le jour et l’heure en plus d’être paramétrable.

Validation

Pour appliquer une règle de validation à un objet, il suffit de passer son nom et ses paramètres dans sa méthode extend. Clair et efficace.

Il est possible de créer ses propres règles de validation. Pour cela on crée une nouvelle fonction de validation que l’on enregistre dans ko.validation.rules. Elle pourra être utilisée en utilisant son nom.

// custom validation rule
ko.validation.rules['arrayIsValid'] = {
   validator: function (valueArray) {
      var isValid = true;
      for (var i = 0, j = valueArray.length; i < j; i++) {
         isValid = isValid && valueArray[i].isValid();
      };

      return isValid;
   },
   message: 'one of the item s array is invalid'
};
ko.validation.registerExtenders();

// use default validation rules
this.maxAttendee = ko.observable(maxAttendee).extend({ required: true, digit: true });
// use custom validation rule
self.Sessions = ko.observableArray([]).extend({ arrayIsValid: "" });

Custom binding

Les custom bindings permettent d’enrichir les bindings par défaut. C’est utile pour intégrer des composants tiers ou gérer des comportements complexe. Contrairement à l’impression lors de la lecture de l’aide, il s’agit d’une fonctionnalité légère et facile d’utilisation.

Les quelques lignes suivantes permettent d’associer le composant jQuery DateTimePicker à un champ simplement. Vous pourrez trouver descustom bindings qui répondront peut-être à votre besoin sur Internet (site officiel ou blogs).

ko.bindingHandlers.DateTimePicker = {
    init: function (element) {
        //(YYYY-MM-DD HH:MM:SS)
        $(element).datetimepicker({ format: 'Y.m.d H:i:s' });
    }
};

Astuce 1: il faut initialiser les paramètres de la validation avant de faire ko.applyBindings(), sinon les paramètres par défaut sont appliqués.

ko.validation.init({ grouping: { deep: true, observable: true }, insertMessages: false, decorateInputElement : true, errorElementClass: 'error', messagesOnModified: false, decorateElementOnModified: false });
ko.applyBindings(ko.validatedObservable(new SessionsViewModel()));

Astuce 2 : ko.toJSON(variable) renvoit une chaine de caractères tandis que ko.toJS(variable) renvoit un object JSON. Ca peut éviter de perdre du temps.

Astuce 3 : Pour débugger, une zone contenant les différents objets manipulés est très pratique.

**

Code

**

// custom binding
ko.bindingHandlers.DateTimePicker = {
    init: function (element) {
        //(YYYY-MM-DD HH:MM:SS)
        $(element).datetimepicker({ format: 'Y.m.d H:i:s' });
    }
};

// custom validation rule
ko.validation.rules['arrayIsValid'] = {
    validator: function (valueArray) {
        var isValid = true;
        for (var i = 0, j = valueArray.length; i < j; i++) {
            isValid = isValid && valueArray[i].isValid();
        };

        return isValid;
    },
    message: 'one of the item s array is invalid'
};
ko.validation.registerExtenders();

function Session(location, eventDate, maxAttendee) {
    this.location = ko.observable(location).extend({ required: true });
    this.eventDate = ko.observable(eventDate).extend({ required: true });
    this.maxAttendee = ko.observable(maxAttendee).extend({ required: true, digit: true });

    this.isValid = function () { return this.location.isValid() && this.eventDate.isValid() && this.maxAttendee.isValid() }
}

function SessionsViewModel() {
    var self = this;
    self.Sessions = ko.observableArray([new Session("", "", "")]).extend({ arrayIsValid: "" });

    // Operations
    self.AddSession = function () {
        self.Sessions.push(new Session("", "", ""));
    }

    self.RemoveSession = function (session) { self.Sessions.remove(session) }

    self.SaveSessions = function () {
        var jSessions = ko.toJS(self.Sessions);
        createListItems(jSessions);// function removed from the article. simple items creation with JSOM.
    }
}

$(function () {
    ExecuteOrDelayUntilScriptLoaded(function(){
        ExecuteOrDelayUntilScriptLoaded(function(){
            ko.validation.init({ grouping: { deep: true, observable: true }, insertMessages: false, decorateInputElement : true, errorElementClass: 'error', messagesOnModified: false, decorateElementOnModified: false });
            ko.applyBindings(ko.validatedObservable(new SessionsViewModel()));
        }, "sp.js");
    }, "core.js");
});

 

<h2>Nombre de session (<span data-bind="text: Sessions().length"></span>)</h2>
<div style="width:560px">
    <div>
        <table>
            <thead><tr>
                <th>Session</th><th>Lieu</th><th>Max participants</th><th></th>
            </tr></thead>
            <tbody data-bind="foreach: Sessions">
                <tr>
                    <td><input readonly="readonly" data-bind='DateTimePicker, value: eventDate, valueUpdate: "afterkeydown"' /></td>
                    <td><input data-bind='value: location, valueUpdate: "afterkeydown"' /></td>
                    <td><input data-bind='value: maxAttendee, valueUpdate: "afterkeydown"' /></td>
                    <td><!-- Remove css sprite -->
                        <a href="#" data-bind="click: $root.RemoveSession">Supprimer</a>
                    </td>
                </tr>    
            </tbody>
        </table>
    </div>

    <div style="padding-top:5px"><!-- Remove css sprite -->
        <a href="#" data-bind="click: $root.AddSession">Nouvelle session</a>
    </div>

    <div style="position:relative; float:right;">
        <button data-bind="visible: Sessions().length > 0, enable: $root.isValid(), click: $root.SaveSessions">Cr&eacute;er les sessions</button>
    </div>
</div>

Etonnant, non ?

Références :

Knockout-Validation

DateTimePicker

Ce que je voulais éviter : Dynamically created controls in ASPNET