Ao usar Handlebars em um ambiente ES6, a limitação do ajudante interno each
de suportar apenas arrays e objetos genéricos se torna inconveniente. Para contornar isso, comecei a registrar minha própria versão do auxiliar each
que suporta arrays, mapas, conjuntos, iteráveis personalizados e objetos genéricos. Esse ajudante está abaixo.
Existe um plano ou vontade de introduzir suporte para esses tipos de listas no ajudante interno each
? Pergunto porque entendo que o Handlebars visa evitar polyfills e imagino que a única maneira de fazer o novo helper funcionar sem comprometer o suporte do navegador seria habilitar progressivamente o suporte para os diferentes tipos de listas dependentes do suporte nativo ou pré-polipreenchido do ambiente por Set
, Map
e Symbol
.
Handlebars.registerHelper("each", function (contexts, options) {
// Throw a runtime exception if options were not supplied.
if (!options) {
throw new Handlebars.Exception("Must pass iterator to #each");
}
// If the "list of contexts" is a function, execute it to get the actual list of contexts.
if (typeof contexts === "function") {
contexts = contexts.call(this);
}
// If data was supplied, frame it.
const data = options.data ? Object.assign({}, options.data, { _parent: options.data }) : undefined;
// Create the string into which the contexts will be handled and returned.
let string = "";
// Create a flag indicating whether or not string building has begun.
let stringExtensionStarted = false;
// Create a variable to hold the context to use during the next string extension. This is done to
// allow iteration through the supplied list of contexts one step out of sync as they are looped
// through later in this helper, ensuring a predictable sequence of value retrieval, string
// extension, value retrieval, string extension...
let nextContext;
// Create a function responsible for expanding the string.
const extendString = (final = false) => {
// If other contexts have been encountered...
if (nextContext) {
// Expand the string using the block function.
string += options.fn(nextContext.value, {
data: data ? Object.assign(data, {
index: nextContext.index,
key: nextContext.key,
first: !stringExtensionStarted,
last: final
}) : undefined,
blockParams: [nextContext.key, nextContext.value]
});
// Note that string extension has begun.
stringExtensionStarted = true;
// If no contexts have been encountered and this is the final extension...
} else if (final) {
// Expand the string using the "else" block function.
string += options.inverse(this);
}
};
// If a list of contexts was supplied...
if (contexts !== null && typeof contexts !== "undefined") {
// Start a counter.
let index = 0;
// If an array list was supplied...
if (Array.isArray(contexts)) {
// For each of the possible indexes in the supplied array...
for (const len = contexts.length; index < len; index++) {
// If the index is in the supplied array...
if (index in contexts) {
// Call the string extension function.
extendString();
// Define the context to use during the next string extension.
nextContext = {
index: index,
key: index,
value: contexts[index]
};
}
}
// If a map list was supplied...
} else if (contexts instanceof Map) {
// For each entry in the supplied map...
for (const [key, value] of contexts) {
// Call the string extension function.
extendString();
// Define the context to use during the next string extension.
nextContext = {
index: index,
key: key,
value: value
};
// Increment the counter.
index++;
}
// If an iterable list was supplied (including set lists)...
} else if (typeof contexts[Symbol.iterator] === "function") {
// Get an iterator from the iterable.
const iterator = contexts[Symbol.iterator]();
// Create a variable to hold the iterator's next return.
let next;
// Do the following...
do {
// Iterate and update the variable.
next = iterator.next();
// If there is anything left to iterate...
if (!next.done) {
// Call the string extension function.
extendString();
// Define the context to use during the next string extension.
nextContext = {
index: index,
key: index,
value: next.value
};
// Increment the counter.
index++;
}
// ... until there is nothing left to iterate.
} while (!next.done);
// If a list other than an array, map, or iterable was supplied...
} else {
// For each key in the supplied object...
for (const key of Object.keys(contexts)) {
// Call the string extension function.
extendString();
// Define the context to use during the next string extension.
nextContext = {
index: index,
key: key,
value: contexts[key]
};
// Increment the counter.
index++;
}
}
}
// Call the string extension a final time now that the last supplied context has been encountered.
extendString(true);
// Return the fully-extended string.
return string;
});
Deve ser possível agora, com #1557
@nknapp parece que a implementação em #1557 não suporta Map
corretamente. Atualmente, produz um item iterado sendo o _entry_ no Map
, que é uma tupla de [key, value]
, enquanto o código de exemplo acima torna o item iterado value
e define @key
, que eu _acho_ é preferível. É preferível para mim!
Além disso, parece que as expressões atualmente não suportam Map
, então você não pode dizer {{person.myMap.myMapKey}}
. Estou me aprofundando mais nesse assunto agora.
Com um acréscimo em lookupProperty
em runtime.js
podemos pesquisar propriedades em Map
s:
lookupProperty: function(parent, propertyName) {
if (parent instanceof Map) {
return parent.get(propertyName)
}
Existe algum apetite para adicionar suporte como este?
@karlvr Acho que vale a pena analisar sua proposta. Mas eu gostaria de discutir isso.
@karlvr , você poderia iniciar um novo problema para o suporte ao mapa. Partes deste problema já estão resolvidas e eu gostaria de ter um começo limpo.
@nknapp muito obrigado por sua rápida resposta; Acabei de fazer um PR com as alterações sugeridas. Podemos discutir lá? #1679
Comentários muito úteis
@karlvr , você poderia iniciar um novo problema para o suporte ao mapa. Partes deste problema já estão resolvidas e eu gostaria de ter um começo limpo.