Champs calculés et formulaires dynamiques avec SharePoint et JavaScript

Cet article fait suite à l’article http://paslatek.net/2015/03/sharepoint/champs-calculs-sharepoint-en-javascript/ qui aborde le concept de champs calculés en utilisant le système de “JS rendering” qui est arrivé avec SharePoint 2013.

Je vais donc présenter dans cet article comment j’ai “amélioré” mes formulaires en ajoutant des interactions entre les différents champs sans pour autant recréer complètement le formulaire. Je vais donc un peu m’éloigner de la notion de “champ calculé” mais je vais vous montrer comment compléter cette fonctionnalité avec un formulaire plus ergonomique/dynamique/professionnel.

Afin de bien comprendre cet article vous devez avoir compris le précédent et des connaissances de l’approche MVVM en JavaScript (KnockoutJS et/ou AngularJS) seront aussi nécessaires pour bien comprendre le code.

Les démos qui suivent vont aborder comment ajouter de l’interaction entre plusieurs champs du même formulaire. Dans les grandes lignes cela consiste à

  • Mettre en place un objet JavaScript qui va pouvoir contenir/manipuler les valeurs de l’ensemble des champs surchargés et même “écouter” les modifications pour mettre à jour automatiquement l’interface du formulaire
  • surcharger le rendu des différents champs avec les éléments nécessaires à la communication avec l’objet JS “central”
  • assembler et démarrer le tout au bon moment

Démo 1 : Colonne recherche dépendante d’une colonne de type Choix

Cette démonstration réponds à un besoin plutôt courant dans les demandes de mes clients et malheureusement SharePoint n’adresse toujours pas cette demande en standard.

Je souhaite avoir un formulaire avec 2 listes déroulantes, dont un choix dans la première va filtrer la liste des choix possibles dans la seconde.

Concrètement reprenons la liste des “Factures” du précédent article:

  • J’ai un champ responsable associé à chaque facture. Ce champ est un champ de type “Recherche” (lookup) pointant sur une liste “Responsables ventes”
  • Dans cette liste “Responsables ventes” j’ai ajouté une colonne de type “Choix” appelée “Region”, afin d’assigner un responsable à une région particulière de la France.
    image
  • Je souhaite donc maintenant que le formulaire de création de “Factures” me propose de choisir d’abord la “Region” (1) pour ensuite choisir le responsable qui va bien (2),
    image

En résumé je veux limiter les données dans la liste déroulante “Responsable” en fonction du choix dans la liste déroulante “Region”

Pour réaliser cela il faut donc compléter 3 étapes:

Démo 1 Etape 1- La classe JavaScript “centrale”

J’ai besoin d’un objet qui va me permettre de:

  1. stocker les valeurs initiales des différents champs (en cas de formulaire en mode edit)
  2. se “brancher” sur les input des différents champs généré par les fonctions de rendu (étape 2) pour récupérer ce qui est saisi et réagir aux sélections de l’utilisateur
  3. faire des appels au modèle objet JavaScript de SharePoint pour récupérer les infos comme la liste des régions où la liste des responsables pour une région donnée
  4. fournir la valeur “finale” à enregistrer, au bon format.

Dans mon cas j’ai donc choisi d’écrire une classe JS qui utilise Knockout.JS pour me simplifier le système de “branchement” sur le DOM en utilisant du databinding. Cela n’est pas du tout obligatoire bien sûr, on pourrait très bien utiliser simplement des sélecteurs et des event jQuery pour faire la même chose. Cependant le databinding (que ce soit avec Knockout.JS ou AngularJS) est tout de même bien pratique dans ce cas pour ajouter simplement et efficacement de l’interactivité à votre formulaire…

La classe contient aussi des fonctions utilisant l’API JS de SharePoint pour les requêtes qui vont bien. Ces fonctions sont déclenchées par

  • le démarrage : récupération des valeurs de la liste de choix Region
  • les changements de valeur de la région sélectionnée : récupération des responsables en fonction de cette région via une Caml Query
var ResponsableVentesJS = {};

ResponsableVentesJS.ResponsableByRegionVM = function () {
    var self = this;
    //une collection pour avoir la liste des régions
    self.regionsList = ko.observableArray([]);
    //une variable qui va stocker la région selectionné via le data binding sur le select
    self.selectedRegion = ko.observable();
    //une collection pour avoir la liste des responsables si une région est selectionnée
    self.responsableList = ko.observableArray([]);
    //une variable qui va stocker le responsable selectionné via le data binding sur le select
    self.currentResponsable = ko.observable();

    //on ecoute le changement de région pour recharger les responsables correspondants
    self.selectedRegion.subscribe(function (newVal) {
        if (newVal!= undefined )
            self.loadResponsableByRegion(false);
    });

   self.loadRegionsList = function () {
        //... utilisation du JSCOM SharePoint
    };

    self.loadResponsableByRegion = function (initialLoading) {
        //...utilisation du JSCOM SharePoint
    };

    self.initFunction = function () {
        //...
        self.loadRegionsList();       
    };

    self.initFunction();
}

 

Le reste du code dans cette classe n’est “qu’assemblage et mécanique” pour gérer correctement le cycle de vie du formulaire (chargement, visibilité des champs, récupération des choix, sélection des valeurs précédente (mode edit)… Je pense que pour peu que vous connaissiez un minimum Knockout, le code est compréhensible de lui-même.
J’en profite tout de même pour faire un petit focus sur les 2 fonctions utilisant l’API JS de SharePoint, si vous êtes curieux, sinon passez à l’étape 2 tout de suite.

La récupération des choix d’une colonne de type “Choice”:

var spctx = new SP.ClientContext.get_current();
var allFields = spctx.get_web().get_availableFields();
spctx.load(allFields);
spctx.executeQueryAsync(
   function (s, e) {
       var fieldEnumerator = allFields.getEnumerator();
       while (fieldEnumerator.moveNext()) {
           var field = fieldEnumerator.get_current();
           var title = field.get_title();
           if (title == "Region") {
               self.regionsList.removeAll();
               var choices = field.get_choices();
               $.each(choices, function (i, v) {
                   self.regionsList.push(v);
               });

           }
       }
                 },
   function (s, e) {
       alert('error loading site fields: ' + e.get_message());
   });

 

La récupération des responsables et le formatage en un objet JS contenant le champ “lookup” au bon format pour la sauvegarde:

self.loadResponsableByRegion = function (initialLoading) {        
        var spctx = new SP.ClientContext.get_current();
        //preparation de la CamlQuery
        var oList = spctx.get_web().get_lists().getByTitle("Responsables Vente");
        var camlQuery = new SP.CamlQuery();
        var query = '<View><Query><Where><Eq><FieldRef Name="Region"/><Value Type="Text">' + self.selectedRegion() + '</Value></Eq></Where></Query></View>';
        camlQuery.set_viewXml(query);
        var collListItem = oList.getItems(camlQuery);

        spctx.load(collListItem, 'Include(ID, Title)');
        //execution de la requette
        spctx.executeQueryAsync(
            function (s, e) {
                self.responsableList.removeAll();
                var listItemEnumerator = collListItem.getEnumerator();
                //iteration des resultats
                while (listItemEnumerator.moveNext()) {
                    var oListItem = listItemEnumerator.get_current();
                    var id = oListItem.get_item("ID");
                    var name = oListItem.get_item("Title");
                    //creation d'un objet de type SP.FieldLookup
                    var lookup = new SP.FieldLookupValue();
                    lookup.set_lookupId(id);
                    //creation de l'objet qui va servir pour le databinding et le stockage de la valeur au bon format
                    var respObj = { id: id, item: lookup, display: name };
                    self.responsableList.push(respObj);                    
                }                
            },
            function (s, e) {
                alert('error loading responsable: ' + e.get_message());
            });
    };

Démo 1 Etape 2- La surcharge du rendu des colonnes

A cette étape on reprend exactement le même système que celui vu dans l’article précédent:

on fournit une fonction de rendu pour nos colonnes ainsi que la fonction de sauvegarde. Dans mon cas j’insère donc des attributs de databinding sur mes tags html pour faire le “branchement” à ma classe “centrale” réalisée à l’étape précédente.

var FacturesJSComputed = {};
FacturesJSComputed.RegionFieldTemplate = function (ctx) {
    var formCtx = SPClientTemplates.Utility.GetFormContextForCurrentField(ctx);
    //on stocke dans une variable "globale" la valeur actuelle 
    FacturesJSComputed.actualRegion = formCtx.fieldValue;
    //on apelle l'objet "central" pou rrécupérer la valeur selectionnée
    formCtx.registerGetValueCallback(formCtx.fieldName, function (c) {
        return FacturesJSComputed.VM.selectedRegion();
    });
    //on envoi notre html qui va bien pour afficher un select bindé sur l'objet "central". 
    //Au passage on en profite pour ajouter quelsues elements pour gérer un peu l'ergonomie (loader ou autre)
    return '<div data-bind="visible: isLoadingRegions">chargement...</div>' + 
        '<select style="display:none;" data-bind="visible: isLoadingRegions()==false, options:regionsList, value:selectedRegion"></select>';

};

FacturesJSComputed.ResponsableFieldTemplate = function (ctx) {
    var formCtx = SPClientTemplates.Utility.GetFormContextForCurrentField(ctx);
    //on stocke dans une variable "globale" la valeur actuelle     
    FacturesJSComputed.actualResponsable = formCtx.fieldValue;
    //on apelle l'objet "central" pou rrécupérer la valeur selectionnée.
    // ici on recupère une propriété qui contient l'objet SharePoint lookup
    formCtx.registerGetValueCallback(formCtx.fieldName, function (c) {
        return FacturesJSComputed.VM.currentResponsable().item;
    });
    //on envoi notre html qui va bien pour afficher un select bindé sur l'objet "central". 
    //Au passage on en profite pour ajouter quelsues elements pour gérer un peu l'ergonomie (loader ou autre)
    return '<div data-bind="visible: isLoadingResponsable">chargement...</div>' +
        '<select style="display:none;" data-bind="visible: isLoadingResponsable()==false, options: responsableList, value:currentResponsable, optionsText:\'display\'"></select>';

};

Notez la déclaration d’un “namespace” en début de code. Cela permet aussi de stocker les valeurs initiales pour y accéder directement depuis l’objet “central” comme des variables globales. Notez aussi dans le html renvoyé l’utilisation des variables de la classe js dans les instructions de binding options et value…

 

Ensuite on “override” le formulaire pour y fournir nos fonctions

//start hook
(function () {

    //object to give to sharepoint to override rendering of the field
    var overrideCtx = {};
    overrideCtx.Templates = {};
    overrideCtx.Templates.Fields = {};

    //surcharge du champ Region
    overrideCtx.Templates.Fields.Region =
         {
             //ici on fourni la fonction de rendu pour chaque cas de formulaire.
             "NewForm": FacturesJSComputed.RegionFieldTemplate,
             "EditForm": FacturesJSComputed.RegionFieldTemplate
         };

    //surcharge du champ Responsable
    overrideCtx.Templates.Fields.Responsable =
         {
             //ici on fourni la fonction de rendu pour chaque cas de formulaire.
             "NewForm": FacturesJSComputed.ResponsableFieldTemplate,
             "EditForm": FacturesJSComputed.ResponsableFieldTemplate
         };

    //apply override
    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideCtx);


})();

 

Démo 1 Etape 3- L’assemblage et le démarrage

Comment utiliser tout ça maintenant !?

Premier problème, il nous faut inclure dans la page NewForm.aspx et EditForm.aspx les fichiers JS nécessaires. Dans mon cas j’ai besoin de JQuery, Knockout et le fichier contenant ma classe “centrale” réalisée à l’étape 1. J’ai choisi pour faire simple de tout stocker dans une liste de documents dédiée nommée TechnicalRes.

Ensuite je crée un fichier htm “header” contenant uniquement la déclaration des include de ces 3 Js

<script type="text/javascript" src="/demojs/TechnicalRes/jquery-1.11.2.min.js"></script>
<script type="text/javascript" src="/demojs/TechnicalRes/knockout-3.2.0.js"></script>
<script type="text/javascript" src="/demojs/TechnicalRes/ResponsableByRegionVM.js"></script>

 

Enfin j’e modifie les pages de formulaire pour y ajouter un webpart Editeur de contenu, auquel j’assigne l’url du fichier htm “header” (1). J’enlève le cadre et le titre de ce webpart pour ne pas voir cette partie (2).

image

A faire donc sur NewForm.aspx et EditForm.aspx

Il faut ensuite inclure le code JS réalisé à l’étape 2 dans la propriété JSLink du webpart du formulaire mais avant ça nous avons un second problème.

En effet dans le “start hook” il manque l’instanciation de notre objet central. Mais cependant il ne faut pas le faire trop tôt. Ici j’ai besoin d’être sûr que :

  • le DOM est chargé
  • JQuery et Knockout sont chargés
  • le modèle objet JS pour SharePoint est chargé

Pour cela le moyen le plus efficace que j’ai trouvé est d’utiliser cette fonction:spBodyOnLoadFunctionNames dans laquelle je passe le nom d’une fonction à moi pour gérer mon démarrage. Dans cette fonction je m’assure aussi que sp.js est bien chargé:

//fonction appelée lorsque le DOM et les JS sont chargés
function startFactureJSformCustomization(){
 //on s'assure d'appliquer notre binding MVVM Knockout que quand le model objet sharepoint et dispo
    SP.SOD.executeOrDelayUntilScriptLoaded(function () {
        if (ResponsableVentesJS) {
            FacturesJSComputed.VM = new ResponsableVentesJS.ResponsableByRegionVM();
            ko.applyBindings(FacturesJSComputed.VM);
        }
    }, 'sp.js');
};

//start hook
(function () {
//...
//...
//...
    //pour ne pas démarrer "trop tôt"
    _spBodyOnLoadFunctionNames.push("startFactureJSformCustomization");

   

})();

 

Il ne reste plus qu’à enregistrer tout ça dans un fichier dans la bibliothèque TechnicalRes et à l’associer au formulaire comme vu lors de l’article précédent.

Démo 2 : Affichage dynamique d’une valeur

Je reprends le cas du calcul du montant TTC à partir des 2 champs Montant HT et TVA présenté dans l’article précédent. Je veux que le montant TTC soit affiché dynamiquement lorsque l’utilisateur est en train de taper les valeurs dans le montant HT et/ou la tva :

image

Je vais profiter de mon objet “central” pour rajouter sur le même principe 3 variables,

var ResponsableVentesJS = {};

ResponsableVentesJS.ResponsableByRegionVM = function () {
    var self = this;
    //***
    //les 2 variables pour les 2 inputs
    self.montantHT = ko.observable();
    self.tva = ko.observable();
    //la fonction de calcul est bindable via un ko.computed
    self.montantTTC = ko.computed(function () {
        var montantHT = parseFloat(self.montantHT());
        var tva = parseFloat(self.tva()) / 100;
        return ((montantHT * tva) + montantHT);
    });

//...
//...
//...

    self.initFunction = function () {
        //...
        self.montantHT(FacturesJSComputed.actualMontantHT);
        self.tva(FacturesJSComputed.actualTva);
        //...
    };

    self.initFunction();
}

 

Toutes connectées à mon rendu html custom via du databinding et 3 fonctions d’override supplémentaires. Le montant HT et la TVA seront des input et le montant TTC une simple span dont le contenu sera mis à jour automatiquement via la fonction de calcul déclenchée à la mise à jour du montant HT et/ou de la TVA (Merci Knockout)

//Montant HT
FacturesJSComputed.MontantHTFieldTemplate = function (ctx) {

    var formCtx = SPClientTemplates.Utility.GetFormContextForCurrentField(ctx);
    FacturesJSComputed.actualMontantHT = formCtx.fieldValue;
    formCtx.registerGetValueCallback(formCtx.fieldName, function (c) {
        return FacturesJSComputed.VM.montantHT();

    });
    return '<input type="Text" data-bind="textInput: montantHT"></input>';
};
//TVA
FacturesJSComputed.TvaFieldTemplate = function (ctx) {

    var formCtx = SPClientTemplates.Utility.GetFormContextForCurrentField(ctx);
    FacturesJSComputed.actualTva = formCtx.fieldValue;
    formCtx.registerGetValueCallback(formCtx.fieldName, function (c) {
        return FacturesJSComputed.VM.tva();

    });
    return '<input type="Text" data-bind="textInput: tva"></input>';
};
//Montant TTC
FacturesJSComputed.MontantTTCFieldTemplate = function (ctx) {

    var formCtx = SPClientTemplates.Utility.GetFormContextForCurrentField(ctx);    
    formCtx.registerGetValueCallback(formCtx.fieldName, function (c) {
        return FacturesJSComputed.VM.montantTTC();      
    });    
    return '<span data-bind="text: montantTTC"></span>' ;
};
Le tout est ajouté à ce qui a été fait dans la démo précédente et le tour est joué !

Conclusion

Vous avez pu constater qu’il est donc possible d’aller vraiment plus loin dans la personnalisation de nos formulaires et de nos colonnes en y ajoutant de l’interactivité autant dans l’interface graphique que dans la logique métier appliquée.
Une fois la mécanique de base en place vous n’aurez plus vraiment de limites techniques à réaliser ces personnalisations.
Pour nuancer mes propos enthousiastes sur cette technique, il faut tout de même se “méfier” de la frontière avec le formulaire “entièrement refait”.
Si votre besoin métier sur le formulaire s’avère vraiment très spécifique et/ou très complexe, il est sans doute plus productif de partir sur un dev de webpart ou d’une App qui va fournir le formulaire “from scratch” qui va bien.
L’inconvénient de cette solution est qu’à chaque changement (ajout/suppression) de colonnes sur la liste il faudra mettre à jour votre webpart / app. Alors que la surcharge du rendu de colonnes en JS permet d’assurer moins d’adhérence à la liste. Tout du moins permet d’ajouter / supprimer d’autres colonnes à postériori via les paramètres de la liste sans que cela impacte votre personnalisation et va donc laisser un peu plus d’autonomie à vos utilisateurs 🙂
Le code final est dispo en ligne ici:
Publié dans SharePoint Tagués avec : , , , , , , , , , , ,
5 commentaires pour “Champs calculés et formulaires dynamiques avec SharePoint et JavaScript
  1. Lionel dit :

    Bonjour Lionel
    J’essaye de faire fonctionner ta solution qui m’intéresse dans le cadre d’une migration d’un ensemble de scripts vers SP2013 (ajout de Knockout; cascade entre les champs).
    J’obtiens malheureusement l’erreur suivante à l’ouverture du NewForm : Demo4.js Line:77 Error : ‘ReponsableVentesJS’ is undefined. J’ai pourtant bien ajouté le lien vers le header.htm dans le Web Content Editor ajouté au formulaire. Si tu peux m’aider ? et merci pour ces articles interressant.
    cdt.
    Lionel

    • Lionel dit :

      Bonjour. Merci pour le retour ! Assurez vous déjà via la devtoolbar de IE/Chrome par exemple que le fichier JS contenant la classe ResponsableVenteJS est bien chargé. On est pas à l’abri d’une erreur d’url dans le header qui fait que les sources JS ne sont pas chargées 🙂
      Le 2nd soucis potentiellement plus vicieux serait que le JSLink soit exécuté avant que le header soit complètement chargé. Ce qui expliquerai l’erreur meme si la source semble chargée via la devtoolbar. Pour le vérifier, rien ne vaut une bonne alerte JS à la fin du fichier contenant la classe ResponsableVenteJS et une alerte JS dans le code utilisé sur le JSLink… à voir qu’elle alerte sort en 1er.
      Dernier point, c’est pas ResponsableVenteVM plutôt qu’elle s’appelle la classe ? 🙂

      • Lionel Lepretre dit :

        Effectivement sur IE11 en debug je ne vois pas les fichiers du header.htm chargés.. mon header est-il incorrect? (Il y a juste les 3 lignes).Pourriez vous le mettre a disposition avec les fichier sources?. J’ai localisé le header.htm dans la même librairie de document que mes autres fichiers Js.
        Je n’appelle pas le fichier JQuery qui est chargé par une feature. mais je vais essayer de le placer dans le répertoire pour coller au mieux a votre exemple.
        Le nom de la classe semble cohérent fichier: ResponsablesByRegionVM.js qui intègre la classe ResponsableVentesJS. Merci pour votre réponse.
        Cdt,
        Lionel L.

  2. Lionel L. dit :

    Effectivement ,mon problème provenait des mauvais chemins.Pour que ça fonctionne:
    Path JSLink : ~sitecollection/Style Library/JSLink/demo4.js
    Path URL : /Style Library/JSLink/header.htm
    et dans le header.htm : ..src= »/Style Library/JSLink/ResponsableByRegionVM.js »…
    Cdt,
    Lionel L.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

*

Verifions que vous êtes un humain * Time limit is exhausted. Please reload CAPTCHA.

Archives

Social

  • Twitter
  • LinkedIn
  • Flux RSS
  • mvp
  • technet
  • Google+