2013-07-11

Les Display Templates par le menu… (1/2)

Ca fait un bout de temps que je n’ai pas publié. Réparons l’outrage. Aujourd’hui, on se coltine les Display Templates. Une nouveauté introduite par SharePoint 2013 d’importance proportionnelle au carré du nombre de XSL que vous avez debuggé pour vos Content Query WebParts.

Là vous me dites “T’es bien intéressant Merly avec tes circonvolutions lexicales mais on vient ici pour du code, pas pour les belles lettres”… Je vous comprends, mais je pense que vous manquez quelque chose Clignement d'œilBref :

Les display templates, qu’est-ce donc ?

Il s’agit de modules de rendu d’items en JavaScript (et HTML / CSS) permettant une restitution des éléments issus des modules de recherche : Content Search Web Parts, Filtres et affineurs ainsi que la restitution de la recherche : par exemple les résultats, les extraits au survol…

On les trouve dans le catalogue des master pages. Donc soit dans les paramètres de site, soit avec SharePoint Designer :

image

image

Note : Dans SharePoint Designer, utilisez la rubrique “All Files” plutôt que “Master Pages” car le filtrage d’affichage de celui-ci ne vous permettra pas de voir les fichiers js.

La description de tous les templates fournis se trouve sur Technet : http://technet.microsoft.com/fr-fr/library/jj944947.aspx

Utilisation des display templates

Les display templates sont affectés soit par paramétrage des webparts, soit par association à des Result Types . Ici, l’exemple d’une Content Search WebPart :

image

Et les associations des result types d’un site de recherche (Paramètres de Site) :

image

Premier display template custom

Pour montrer le principe de personnalisation par Display Template, un exemple concret : Je veux réaliser un affichage au format “carte de visite” de mes éléments avec une image / miniature / icône, un titre et un descriptif.

On commence par créer une page et on insère la WebPart de Content Search. Pour tester les données, je vous conseille d’utiliser le template “Diagnostic” et de le mapper sur les propriétés que vous voulez. Affinez votre recherche jusqu’à obtenir les données souhaitées :

image

Maintenant qu’on a le paramétrage, y’a plus qu’à Sourire

Créer le template

On ne va pas se fatiguer : A l’aide de SharePoint Designer, on copie un template existant (je vous conseille le “2 lines”, facile à interpréter) et on l’adapte à notre situation après l’avoir renommé. Dissection :

   1:  // Initialisation de la collecte de statistiques (cf. http://stackoverflow.com/questions/4177001/what-does-this-javascript-code-do/7543257#7543257)
   2:  function ULS16h(){var o=new Object;o.ULSTeamName="Search Server";o.ULSFileName="Item_TwoLines.js";return o;}
   3:   
   4:  // Extraction des paramètres et génération de l'élément (Note : Remplacer le Guid par votre identifiant propre partout dans la source).
   5:  function DisplayTemplate_dee7d9226aa44ed7b709d342fec837ee(ctx) {ULS16h:;
   6:      var ms_outHtml=[];
   7:      var cachePreviousTemplateData = ctx['DisplayTemplateData'];
   8:      ctx['DisplayTemplateData'] = new Object();
   9:      DisplayTemplate_dee7d9226aa44ed7b709d342fec837ee.DisplayTemplateData = ctx['DisplayTemplateData'];
  10:      
  11:      // Initialisation des paramètres du template avant mise en cache.
  12:      ctx['DisplayTemplateData']['TemplateUrl']='~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fContent Web Parts\u002fItem_TwoLines.js';
  13:      ctx['DisplayTemplateData']['TemplateType']='Item';
  14:      ctx['DisplayTemplateData']['TargetControlType']=['Content Web Parts'];
  15:      
  16:      // Initialisation :
  17:      this.DisplayTemplateData = ctx['DisplayTemplateData'];
  18:      
  19:      // Initialisation des paramètres du tempalte avant mise en cache.
  20:      ctx['DisplayTemplateData']['ManagedPropertyMapping']={'Link URL':['Path'], 'Line 1':['Title'], 'Line 2':[], 'FileExtension':null, 'SecondaryFileExtension':null};
  21:      
  22:      var cachePreviousItemValuesFunction = ctx['ItemValues'];
  23:      ctx['ItemValues'] = function(slotOrPropName) {ULS16h:;
  24:          return Srch.ValueInfo.getCachedCtxItemValue(ctx, slotOrPropName)
  25:      };
  26:   
  27:      ms_outHtml.push('','');
  28:      
  29:      // Génération d'un ID prefixant les sous-éléments
  30:      var encodedId = $htmlEncode(ctx.ClientControl.get_nextUniqueId() + "_2lines_");
  31:   
  32:      // Récupération effective des paramètres de la WebPart "Display Template"-ée.
  33:      var linkURL = $getItemValue(ctx, "Link URL");
  34:      linkURL.overrideValueRenderer($urlHtmlEncode);
  35:      var iconURL = Srch.ContentBySearch.getIconSourceFromItem(ctx.CurrentItem);
  36:      var line1 = $getItemValue(ctx, "Line 1");
  37:      var line2 = $getItemValue(ctx, "Line 2");
  38:      line1.overrideValueRenderer($contentLineText);
  39:      line2.overrideValueRenderer($contentLineText);
  40:      
  41:      // Préparation des Id des différents éléments. Ils peuvent ainsi être adressés directement en JS par la suite.
  42:      var containerId = encodedId + "container";
  43:      var pictureLinkId = encodedId + "pictureLink";
  44:      var pictureId = encodedId + "picture";
  45:      var dataContainerId = encodedId + "dataContainer";
  46:      var line1LinkId = encodedId + "line1Link";
  47:      var line1Id = encodedId + "line1";
  48:      var line2Id = encodedId + "line2";
  49:      
  50:      // Envoi de la source générée pour le rendu.
  51:      ms_outHtml.push(''
  52:          ,'        <div class="cbs-Item" id="', containerId ,'" data-displaytemplate="Item2Lines">'
  53:          ,'            <a class="cbs-ItemLink" title="', $htmlEncode(line1.defaultValueRenderer(line1)) ,'" id="', pictureLinkId ,'">'
  54:          ,'                <img class="cbs-Thumbnail" src="', $urlHtmlEncode(iconURL) ,'" alt="', $htmlEncode(line1.defaultValueRenderer(line1)) ,'" id="', pictureId ,'" />'
  55:          ,'            </a>'
  56:          ,'            <div class="cbs-Detail" id="', dataContainerId ,'">'
  57:          ,'                <a class="cbs-Line1Link ms-noWrap ms-displayBlock" href="', linkURL ,'" title="', $htmlEncode(line1.defaultValueRenderer(line1)) ,'" id="', line1LinkId ,'">', line1 ,'</a>'
  58:      );
  59:      if(!line2.isEmpty)
  60:      {
  61:          ms_outHtml.push(''
  62:              ,'                <div class="cbs-Line2 ms-noWrap" title="', $htmlEncode(line2.defaultValueRenderer(line2)) ,'" id="', line2Id ,'">', line2 ,'</div>'
  63:          );
  64:      }
  65:      ms_outHtml.push(''
  66:          ,'                </div>'
  67:          ,'        </div>'
  68:          ,'    '
  69:      );
  70:      
  71:      // "Nettoyage" de l'objet contexte emprunté.
  72:      ctx['ItemValues'] = cachePreviousItemValuesFunction;
  73:      ctx['DisplayTemplateData'] = cachePreviousTemplateData;
  74:      return ms_outHtml.join('');
  75:  }
  76:   
  77:  // Inscription du template pour affichage (permet l'affichage. Le template reste visible mais ne fonctionnera pas si cette fonction est incorrecte).
  78:  function RegisterTemplate_dee7d9226aa44ed7b709d342fec837ee() {ULS16h:;
  79:      if ("undefined" != typeof (Srch) &&"undefined" != typeof (Srch.U) &&typeof(Srch.U.registerRenderTemplateByName) == "function") {
  80:          Srch.U.registerRenderTemplateByName("TwoLines", DisplayTemplate_dee7d9226aa44ed7b709d342fec837ee);
  81:      }
  82:   
  83:      if ("undefined" != typeof (Srch) &&"undefined" != typeof (Srch.U) &&typeof(Srch.U.registerRenderTemplateByName) == "function") {
  84:          Srch.U.registerRenderTemplateByName("~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fContent Web Parts\u002fItem_TwoLines.js", DisplayTemplate_dee7d9226aa44ed7b709d342fec837ee);
  85:      }
  86:   
  87:      $includeLanguageScript("~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fContent Web Parts\u002fItem_TwoLines.js", "~sitecollection/_catalogs/masterpage/Display Templates/Language Files/{Locale}/CustomStrings.js");
  88:  }
  89:   
  90:  RegisterTemplate_dee7d9226aa44ed7b709d342fec837ee();
  91:   
  92:  // Etape d'initialisation.
  93:  if (typeof(RegisterModuleInit) == "function" && typeof(Srch.U.replaceUrlTokens) == "function") {
  94:      RegisterModuleInit(Srch.U.replaceUrlTokens("~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fContent Web Parts\u002fItem_TwoLines.js"), RegisterTemplate_dee7d9226aa44ed7b709d342fec837ee);
  95:  }

Faisons donc une première passe d’adaptation : On remplace “TwoLines” et “2line” par des noms adaptés. et les liens / noms de fichier par ceux de notre nouveau template. Ne touchez pour l’instant pas au code généré ou aux variables récupérées.


Premier test


Sauvegardez, modifiez le Titre de l’élément pour qu’il corresponde à ce que vous souhaitez et retournez sur votre page de test. Rafraichissez et affichez les propriétés de la WebPart : Votre Template doit apparaitre dans la liste et avoir le même comportement que le template “TwoLines” existant.


imageTADA…


Et c’est le moment d’ajouter de la valeur à notre template : Modifier les paramètres et le contenu.


Modifier les paramètres


Reprenons la source du template. Modifions d’abord le mapping des propriétés :

ctx['DisplayTemplateData']['ManagedPropertyMapping']={'Link URL':['Path'], 'Thumbnail':[], 'Title Line':['Title'], 'Summary':['Description'], 'FileExtension':null, 'SecondaryFileExtension':null};









Le format est un tableau associatif de ‘Nom de Propriété déclaré’:[‘Champ associé par défaut’]


Ensuite, on modifie l’extraction des données :


   1:  var thumbnail = $getItemValue(ctx, "Thumbnail");
   2:  var titleLine = $getItemValue(ctx, "Title Line");
   3:  var summary = $getItemValue(ctx, "Summary");
   4:  thumbnail.overrideValueRenderer($contentLineText);
   5:  titleLine.overrideValueRenderer($contentLineText);
   6:  summary.overrideValueRenderer($contentLineText);




Notez que le 2e paramètre de $getItemValue est le nom de propriété déclaré. Enfin, modifiez le rendu. Par exemple, en affichant les propriétés directement :


   1:  // Rendu poussé dans ms_outHtml :
   2:  ms_outHtml.push(
   3:      '<p>linkURL : ' + linkURL + '</p>',
   4:      '<p>iconURL : ' + iconURL + '</p>',
   5:      '<p>thumbnail : ' + thumbnail + '</p>',
   6:      '<p>titleLine : ' + titleLine + '</p>',
   7:      '<p>summary : ' + summary + '</p>',
   8:      '<hr />');



Sauvegardez et dans les paramètres de site, n’oubliez pas de mettre à jour les propriétés du template et notamment “Managed Property Mappings” pour propager les propriété personnalisées.


'Link URL'{Link URL}:'Path','Thumbnail'{Thumbnail}:'','Title Line'{Item Title}:'Title','Summary'{Summary}:'','FileExtension','SecondaryFileExtension'


Attention, contrairement à ce que la documentation prétend, le découpage est ‘Nom déclaré de la propriété (cf. ci-dessus)’{Libellé affiché}:’Champ par défaut’ mais vous le verrez rapidement pendant ce test pour peu que vous preniez la peine de faire varier les valeurs… On peut désormais valider le rendu :


image


On retourne travailler le rendu et avec quelques modification du style et autres (OK, dans cet exemple j’ai fait le goret en injectant mon style directement sur les balises mais vous n’êtes pas obligé de faire moche en m’imitant Clignement d'œil) :


ms_outHtml.push(
    '<div class="cbsCardItem" id="', containerId ,'" data-displaytemplate="ItemCard" style="border: 1px solid #FFBB99;width: 500px;height: 200px;padding: 0;margin: 0;">',
    '    <a class="cbsCardLink" title="Cliquez pour afficher l\'élément" href="', $urlHtmlEncode(linkURL), '" style="color: inherit;">',
    '        <img class="cbs-Thumbnail" src="', $urlHtmlEncode((thumbnail.isEmpty) ? iconURL : thumbnail) ,'" alt="', $htmlEncode(titleLine.defaultValueRenderer(titleLine)) ,'" id="', pictureId ,'" style="height: 200px;width: auto;float: left;display: inline-block; margin-right: 5px;" />',
    '        <div class="cbs-TextContainer" id="', dataContainerId ,'" style="background-color: #99DDFF; height: 100%;">',
    '            <h1>', titleLine ,'</h1>',
    '            <p>', summary ,'</p>',
    '        </div>',
    '    </a>',
    '</div>',
    '<hr />');



Et hop, j’ai mon premier Template un tant soit peu “pêchu” :


image


Le control : un peu plus de personnalisation


Bien. Si vous m’avez suivi jusqu’ici. Déjà je vous en remercie, de plus, ça fait chaud au cœur et enfin, sachez qu’on n’a vu que 25 % du truc mais que pour le reste, ça en découle grandement.


Donc, on sait mettre en forme plutôt aisément un élément en mappant un template qu’on génère en JS sur des propriétés assignables à la volée sur une WebPart. Finissons le travail en traitant du groupe d’éléments. En effet, un détail n’aura pas échappé à votre perspicacité légendaire (ne rougissez pas, c’est sincère) :


image










Et oui, on s’est occupé de “Item”, mais pas encore de cet attribut “Control”, qui permet pour l’instant de faire une liste, paginée ou non, ou un diaporama basique et dont on retrouve trace dans le catalogue “Display Templates > Content Web Parts” : Control_List.js et consorts…


Bingo ! On s’y attèle de suite (à la différence de celle de maintien qui n’aurait sa place que sur un blog médical, loin de mon propos… Bref.) !


Scénario cible


J’ai donc un affichage en cartes, et bien, je vais les faire afficher en forme de pile avec possibilité de les consulter par survol à la souris.


Retour dans SharePoint Designer ou je duplique le template de Contrôle de liste et en fait un Control_Stack.js avec modification des propriétés comme on en a désormais l’habitude. Je vous épargne la dissection étant donné que le contenu est proche de celui de l’item à la différence majeur qu’on adresse ici l’ensemble des éléments et qu’il faut appeler les template d’items dans le push de génération.


Superposition des cartes


Première manipulation sur le contenu : Transformer la liste ul / li en un esemble de div se chevauchant. Attention, là aussi, j’ai fait vite en utilisant une variable globale qui causera un conflit si plusieurs contrôles de ce type sont utilisés sur a même page. Vous pouvez utiliser l’objet ctx pour générer un id unique et traiter ainsi proprement votre empilement.


   1:  var g_ItemCount = 0;
   2:  var ListRenderRenderWrapper = function(itemRenderResult, inCtx, tpl)
   3:  {ULS9sP:;
   4:      var iStr = [];
   5:      iStr.push('<div class="itemToPop" style="position: absolute; top: ', "" + (g_ItemCount*20), 'px; left: ', "" + (g_ItemCount*20), 'px">');
   6:      iStr.push('<!--', g_ItemCount++ , '-->');
   7:      iStr.push(itemRenderResult);
   8:      iStr.push('</div>');
   9:      return iStr.join('');
  10:  }
  11:  ctx['ItemRenderWrapper'] = ListRenderRenderWrapper;
  12:  ms_outHtml.push(''
  13:  ,'    <div class="cbs-List" style="position: relative;">'
  14:  ,''
  15:  ,'            ', ctx.RenderGroups(ctx) ,''
  16:  ,'        </div>'
  17:  );



Un petit test pour valider le comportement. Ca semble pas mal :


image



Astuce : Utilisation de jQuery dans les Display Templates


OK, à ce stade, pour gérer les survols et autres, j’aimerais bien utiliser jQuery… Qu’à cela ne tienne : J’ajoute les liens dans l’en-tête (au passage, inclusion d’une CSS personnalisée, ça ne mange pas de pain)


   1:  var script = document.createElement('script');
   2:  script.src = 'http://ajax.microsoft.com/ajax/jQuery/jquery-1.9.1.min.js';
   3:  script.type = 'text/javascript';
   4:  document.getElementsByTagName('head')[0].appendChild(script);
   5:  var link_tag = document.createElement('link');
   6:  link_tag.setAttribute('rel', 'stylesheet');
   7:  link_tag.setAttribute('type', 'text/css');
   8:  link_tag.setAttribute('href', '/_layouts/TME/SimpleLinks.css');
   9:  document.getElementsByTagName('head')[0].appendChild(link_tag);



Et j’ajoute benoitement l’appel :


   1:  // Cette ligne est déjà présente dans le template...
   2:  ctx['DisplayTemplateData'] = cachePreviousTemplateData;
   3:   
   4:  // Gestion du passage au premier plan :
   5:  $(document).ready(function() {
   6:      // find the div elements and hook the hover event
   7:      $('.itemToPop').hover(function() {
   8:          $(this).css('z-index',2)
   9:      }, function () {
  10:          $(this).css('z-index',1)
  11:      });
  12:  });




Note : Il est possible que ce code ne fonctionne pas directement (erreur jQuery inconnu ou $ est null ou n’est pas objet…). Dans ce cas, on peut utiliser la propriété “OnPostRender” de l’objet de contexte :


   1:  // Cette ligne est toujours là pour situer l'action :)
   2:  ctx['DisplayTemplateData'] = cachePreviousTemplateData;
   3:   
   4:  ctx.OnPostRender = [];
   5:  ctx.OnPostRender.push(function(){ /* invoke your plugin code here to ensure the plugin has the content to work against */
   6:  $(document).ready(function() {
   7:      // find the div elements and hook the hover event
   8:      $('.itemToPop').hover(function() {
   9:          $(this).css('z-index',2)
  10:      }, function () {
  11:          $(this).css('z-index',1)
  12:      });
  13:   })
  14:  });



Et zou, on vide les caches, on ouvre ses chakras et on contemple avec béatitude le rendu dynamique Sourire


image


Ceci clôture la première partie du dossier “Display Templates”. Vous avez matière à expérimenter. Rendez-vous d’ici quelques jours pour la 2e partie plus axée sur la recherche : affineurs, filtres et résultat de recherche au menu avec quelques pépites surprises…