2016-12-02 4 views
1

Je commence juste avec handlebars et j'essaye de faire un simple double pour boucler afin d'avoir tout le temps du jour avec 15 intervalles de minutes. Une chose très étrange se produit où si l'enfant et le parent ont les mêmes valeurs, l'objet view est renvoyé à la place. Ceci est mon code:../ceci retourne l'objet de vue à l'intérieur de la boucle interne quand le parent et l'enfant ont la même valeur

var handlebarsExpress = require('express-handlebars').create({ 
    helpers: { 
     for: function(from, to, incr, block) { 
      var accum = ''; 
      for(var i = from; i <= to; i += incr) { 
       accum += block.fn(i); 
      } 
      return accum; 
     } 
    } 
}); 

app.engine('handlebars', handlebarsExpress.engine); 
app.set('view engine', 'handlebars'); 

... 
... 

Dans le fichier de vue (sth.handlebars) J'ai ceci:

<div class="row columns large-12"> 
    {{#for 0 23 1}} 
     {{#for 0 45 15}} 
      {{log ../this}} 
      <span>{{../this}}:{{this}}</span><br> 
     {{/for}} 
    {{/for}} 
</div> 

La sortie html est quelque chose comme ceci:

[object Object]:0 
0:15 
0:30 
0:45 
1:0 
... 
... 
13:45 
14:0 
14:15 
14:30 
14:45 
15:0 
[object Object]:15 
15:30 
15:45 
16:0 
16:15 

Les rapports aide log la à la suite dans le terminal:

{ settings: 
    { 'x-powered-by': true, 
    etag: 'weak', 
    'etag fn': [Function: wetag], 
    env: 'development', 
    'query parser': 'extended', 
    'query parser fn': [Function: parseExtendedQueryString], 
    'subdomain offset': 2, 
    'trust proxy': false, 
    'trust proxy fn': [Function: trustNone], 
    view: [Function: View], 
    views: '/home/elsa/Projects/rental-borg/project/views', 
    'jsonp callback name': 'callback', 
    'view engine': 'handlebars', 
    port: 8765 }, 
    flash: { info: undefined, error: undefined }, 
    _locals: { flash: { info: undefined, error: undefined } }, 
    cache: false } 
0 
0 
0 
1 
1 
1 
1 
2 
2 
2 
2 
3 
3 
3 
3 
4 
... 
... 
13 
13 
13 
13 
14 
14 
14 
14 
15 
{ settings: 
    { 'x-powered-by': true, 
    etag: 'weak', 
    'etag fn': [Function: wetag], 
    env: 'development', 
    'query parser': 'extended', 
    'query parser fn': [Function: parseExtendedQueryString], 
    'subdomain offset': 2, 
    'trust proxy': false, 
    'trust proxy fn': [Function: trustNone], 
    view: [Function: View], 
    views: '/home/elsa/Projects/rental-borg/project/views', 
    'jsonp callback name': 'callback', 
    'view engine': 'handlebars', 
    port: 8765 }, 
    flash: { info: undefined, error: undefined }, 
    _locals: { flash: { info: undefined, error: undefined } }, 
    cache: false } 
15 
15 
16 
16 
16 
16 
... 
23 
23 
23 
23 

Il est évident que lorsque this et ../this sont égaux, ../this renvoie effectivement quelque chose comme ../../this qui est l'objet de vue lui-même, je suppose. Cela se produit deux fois dans mon cas spécifique, 00:00 et 15:15.

Est-ce un bug?

EDIT: J'ai résolu le problème avec la nouvelle fonction de paramètres de bloc de Handlebars 3, mais la question initiale demeure. C'est ce que je l'ai fait pour résoudre le problème:

fichier .handlebars:

{{#for 0 23 1 as |hour|}} 
    {{#for 0 45 15 as |minute|}} 
     <span>{{hour}}:{{minute}}</span> 
    {{/for}} 
{{/for}} 

et l'aide:

for: function(from, to, incr, block) { 
    var args = [], options = arguments[arguments.length - 1]; 
    for (var i = 0; i < arguments.length - 1; i++) { 
     args.push(arguments[i]); 
    } 

    var accum = ''; 
    for(var i = from; i <= to; i += incr) { 
     accum += options.fn(i, {data: options.data, blockParams: [i]}); 
    } 
    return accum; 
}, 

Répondre

4

Ce fut une découverte intrigante et je voulais vraiment trouver la raison de cette comportement apparemment étrange. J'ai creusé un peu dans le Handlebars source code et j'ai trouvé le code pertinent sur le line 176 of lib/handlebars/runtime.js.

Handlebars gère une pile d'objets de contexte dont l'objet supérieur est la référence {{this}} dans le bloc de gabarit Handlebars en cours d'exécution. Lorsque des blocs imbriqués sont rencontrés dans le modèle en cours d'exécution, un nouvel objet est poussé vers la pile. C'est ce qui permet le code comme suit:

{{#each this.list}} 
    {{this}} 
{{/each}} 

Sur la première ligne, this est un objet avec une propriété dénombrable « liste ». Sur la ligne deux, this est l'élément actuellement itératif de l'objet list. Comme la question le montre, Handlebars nous permet d'utiliser ../ pour accéder aux objets contextuels plus profonds dans la pile. Dans l'exemple ci-dessus, si nous voulons accéder à l'objet list à partir de l'aide #each, nous aurions dû utiliser {{../this.list}}. En l'absence de ce bref résumé, la question demeure: pourquoi notre pile de contexte semble-t-elle casser lorsque la valeur de l'itération actuelle de la boucle externe for est égale à celle de la boucle interne for?

Le relevant code de la source guidons est la suivante:

let currentDepths = depths; 
if (depths && context != depths[0]) { 
    currentDepths = [context].concat(depths); 
} 

depths est l'empilement interne d'objets de contexte, context est l'objet passé au bloc en cours d'exécution, et currentDepths est la pile de contexte qui est fait disponible pour le bloc en cours d'exécution. Comme vous pouvez le voir, le context actuel est poussé vers la pile disponible seulement sicontext n'est pas loosely equal au sommet actuel de la pile, depths[0].

Appliquons cette logique au code de la question.

Lorsque le contexte du bloc #for externe est 15 et le contexte du bloc #for interne est 0:

  • depths est: [15, {ROOT_OBJECT}] (où {ROOT_OBJECT} signifie que l'objet qui est l'argument de la appel de méthode modèle.)
  • Becuase 0 != 15, currentDepths devient: [0, 15, {ROOT_OBJECT}].
  • Dans le bloc #for intérieur du modèle, {{this}} est 0, {{../this}} est 15 et {{../../this}} est {} ROOT_OBJECT.

Cependant, lorsque les et#for internes blocs externes ont chacun une valeur de contexte de 15, nous obtenons les éléments suivants:

  • depths est: [15, {ROOT_OBJECT}]. Parce que 15 == 15, currentDepths = depths = [15, {ROOT_OBJECT}].
  • Dans le bloc #for intérieur du modèle, {{this}} est 15, {{../this}} est {} ROOT_OBJECT et {{../../this}} est undefined.

C'est pourquoi il semble que votre {{../this}} saute un niveau lorsque les #for internes et externes blocs ont la même valeur. C'est en fait parce que la valeur de l'intérieur #for est pas poussé à la pile de contexte!

C'est à ce stade que nous devrions nous demander pourquoi Handlebars se comporte de cette façon et déterminer s'il s'agit d'une fonctionnalité ou d'un bogue.

Il se trouve que ce code a été ajouté intentionnellement pour résoudre un issue que les utilisateurs rencontraient avec les Handlebars.La question peut être démontrée à titre d'exemple:

En supposant un objet contexte de:

{ 
    config: { 
     showName: true 
    }, 
    name: 'John Doe' 
} 

Les utilisateurs trouvent le cas d'utilisation ci-dessous pour être contre-intuitif:

{{#with config}} 
    {{#if showName}} 
     {{../../name}} 
    {{/if}} 
{{/with}} 

La question spécifique a été avec la nécessité pour le double ../ d'accéder à l'objet racine: {{../../name}} plutôt que {{../name}}. Les utilisateurs ont estimé que puisque l'objet contexte dans {{#if showName}} était l'objet config, puis intensifier un niveau, ../, devrait accéder au "parent" de config - l'objet racine. La raison pour laquelle deux étapes étaient nécessaires était que Handlebars créait un objet de pile de contexte pour chaque assistant de bloc. Cela signifie que deux étapes sont nécessaires pour accéder au contexte racine; la première étape obtient le contexte de {{#with config}}, et la deuxième étape obtient le contexte de la racine.

A commit a été faite qui empêche la poussée d'un objet de contexte dans la pile de contexte disponible lorsque le nouvel objet de contexte est lâchement égale à l'objet au sommet de la pile de contexte. Le code responsable est le code source que nous avons examiné ci-dessus. À partir de version 4.0.0 de Handlebars.js, notre exemple de configuration échouera. Il nécessite maintenant seulement une seule étape ../.

Pour en revenir à l'exemple de code dans la question initiale, la raison pour laquelle le 15 du bloc #for externe est déterminé comme égal à la 15 dans le bloc #for intérieur est due à la façon dont les types de nombre sont comparés en JavaScript; deux objets du type Number sont égaux s'ils ont chacun la même valeur . Ceci est en contraste avec le type d'objet, pour lequel deux objets sont égaux seulement s'ils référencent le même objet en mémoire. Cela signifie que si nous réécrivions l'exemple de code original pour utiliser les types Object au lieu des types Number pour les contextes, nous ne satisferions jamais à l'instruction de comparaison conditionnelle et toujours aurait la pile de contexte attendue dans notre bloc interne #for.

La boucle dans notre aide serait mis à jour pour passer un type d'objet que le contexte du cadre crée:

for(var i = from; i <= to; i += incr) { 
    accum += block.fn({ value: i }); 
} 

Et notre modèle aurait maintenant besoin d'accéder à la propriété pertinente de cet objet:

{{log ../this.value}} 
<span>{{../this.value}}:{{this.value}}</span><br> 

Avec ces modifications, votre code devrait fonctionner comme prévu.

Il est quelque peu subjectif de déclarer s'il s'agit ou non d'un bug avec Handlebars. Le conditionnel a été ajouté intentionnellement et le comportement résultant fait ce qu'il était destiné à faire. Cependant, je trouve difficile d'imaginer un cas dans lequel ce comportement serait attendu ou souhaitable lorsque les contextes impliqués sont des primitives et pas Les types d'objets. Il peut être raisonnable pour le code Handlebars d'effectuer la comparaison sur les types d'objet seulement. Je pense qu'il y a un cas légitime ici à open an issue.

+0

Super réponse, merci beaucoup, j'étais aussi curieux mais je n'ai pas eu le temps d'enquêter – hakermania