Introduction
Handlebars est le moteur de templates utilise par FoundryVTT pour generer le HTML des interfaces. Il permet de separer la logique de presentation des donnees, rendant le code plus maintenable.
Handlebars offre une syntaxe simple et lisible pour inserer des donnees dynamiques dans le HTML. Il est "logic-less" par conception : la logique complexe reste dans le JavaScript, pas dans les templates.
Syntaxe de base
Interpolation de variables
Utilisez les doubles accolades pour afficher une valeur :
<!-- Variable simple -->
<h1>{{actor.name}}</h1>
<!-- Acces aux proprietes imbriquees -->
<span>{{system.attributes.hp.value}}</span>
<!-- Variables avec espaces (notation crochet) -->
<span>{{system.abilities.[strength].value}}</span>
Echappement HTML
Par defaut, Handlebars echappe le HTML pour la securite. Utilisez les triples accolades pour du HTML brut :
<!-- Echappe les caracteres speciaux (securise) -->
<p>{{description}}</p>
<!-- Resultat: <b>Texte</b> -->
<!-- HTML non echappe (pour le contenu enrichi) -->
<div class="biography">{{{enriched.biography}}}</div>
<!-- Resultat: <b>Texte</b> -->
N'utilisez les triples accolades {{{ }}} que pour du contenu
prealablement securise via TextEditor.enrichHTML().
Commentaires
<!-- Commentaire HTML (visible dans le source) -->
{{!-- Commentaire Handlebars (invisible dans le rendu) --}}
Conditions et boucles
Bloc {{#if}}
<!-- Condition simple -->
{{#if editable}}
<input type="text" name="name" value="{{name}}">
{{else}}
<span>{{name}}</span>
{{/if}}
<!-- Condition inverse avec unless -->
{{#unless locked}}
<button data-action="edit">Modifier</button>
{{/unless}}
Le bloc {{#if}} considere comme faux : false, undefined,
null, "", 0, et les tableaux vides [].
Bloc {{#each}}
<!-- Iteration sur un tableau -->
<ul class="items-list">
{{#each items}}
<li data-item-id="{{this.id}}">
<img src="{{this.img}}" alt="{{this.name}}">
<span>{{this.name}}</span>
</li>
{{else}}
<li class="empty">Aucun objet</li>
{{/each}}
</ul>
<!-- Iteration sur un objet -->
<div class="abilities">
{{#each abilities}}
<div class="ability" data-ability="{{@key}}">
<label>{{@key}}</label>
<span>{{this.value}}</span>
</div>
{{/each}}
</div>
Variables speciales dans {{#each}}
{{#each items}}
<!-- @index : index numerique (0, 1, 2...) -->
<span class="index">{{@index}}</span>
<!-- @key : cle de l'objet (pour les objets) -->
<span class="key">{{@key}}</span>
<!-- @first : true si premier element -->
{{#if @first}}<span class="badge">Premier</span>{{/if}}
<!-- @last : true si dernier element -->
{{#if @last}}<span class="badge">Dernier</span>{{/if}}
<!-- this : element courant -->
<span>{{this.name}}</span>
<!-- ../ : acces au contexte parent -->
<span>Proprietaire: {{../actor.name}}</span>
{{/each}}
Bloc {{#with}}
Change le contexte pour eviter la repetition :
<!-- Sans #with -->
<div>
<span>{{system.attributes.hp.value}}</span>
<span>{{system.attributes.hp.max}}</span>
<span>{{system.attributes.hp.temp}}</span>
</div>
<!-- Avec #with -->
{{#with system.attributes.hp}}
<div>
<span>{{value}}</span>
<span>{{max}}</span>
<span>{{temp}}</span>
</div>
{{/with}}
Helpers Foundry
FoundryVTT fournit de nombreux helpers pour faciliter le developpement.
Traduction avec {{localize}}
<!-- Traduction simple -->
<label>{{localize "MON_SYSTEME.Attributes.Strength"}}</label>
<!-- Traduction avec interpolation -->
<span>{{localize "MON_SYSTEME.LevelUp" level=newLevel}}</span>
<!-- Fichier lang: "LevelUp": "Niveau {level} atteint !" -->
Editeur de texte riche
<!-- Editeur simple -->
{{editor content=system.biography
target="system.biography"
button=true
editable=editable}}
Selecteur de fichier
<!-- Selecteur d'image -->
{{filePicker type="image"
target="img"
value=actor.img}}
Checkbox et attributs conditionnel
<!-- Attribut checked conditionnel -->
<input type="checkbox" name="system.active" {{checked system.active}}>
<!-- Attribut disabled conditionnel -->
<input type="text" {{disabled (not editable)}}>
<!-- Attribut selected pour les options -->
<option value="fire" {{selected (eq system.damageType "fire")}}>Feu</option>
Select avec options
<!-- Select avec helper selectOptions -->
<select name="system.size">
{{selectOptions sizeOptions selected=system.size}}
</select>
<!-- Options avec groupes -->
<select name="system.skill">
{{selectOptions skillOptions
selected=system.skill
blank="Choisir..."
labelAttr="label"
valueAttr="id"}}
</select>
Helpers de comparaison
<!-- Egalite -->
{{#if (eq system.type "weapon")}}
<span>C'est une arme</span>
{{/if}}
<!-- Difference -->
{{#if (ne status "dead")}}
<span>Encore en vie</span>
{{/if}}
<!-- Comparaisons numeriques -->
{{#if (gt hp.value 0)}}<span>En vie</span>{{/if}}
{{#if (gte level 5)}}<span>Niveau 5+</span>{{/if}}
{{#if (lt hp.value hp.max)}}<span>Blesse</span>{{/if}}
{{#if (lte uses 0)}}<span>Epuise</span>{{/if}}
<!-- Operateurs logiques -->
{{#if (and isOwner editable)}}
<button>Modifier</button>
{{/if}}
{{#if (or isGM isOwner)}}
<section class="secrets">{{secrets}}</section>
{{/if}}
{{#if (not locked)}}
<button>Deverrouiller</button>
{{/if}}
Helpers mathematiques
<!-- Calculs -->
<span>Modificateur: {{numberSign (floor (divide (subtract str 10) 2))}}</span>
<!-- Format de nombre -->
<span>{{numberFormat price decimals=2 sign=false}}</span>
<!-- Signe du nombre (+/-) -->
<span class="modifier">{{numberSign modifier}}</span>
<!-- Resultat: +3 ou -2 -->
Helpers de formulaire DataModel
<!-- Input automatique base sur le schema -->
{{formInput fields.hp.value value=system.hp.value name="system.hp.value"}}
<!-- Groupe de champ complet -->
{{formGroup fields.biography
value=system.biography
name="system.biography"
label="Biographie"}}
Partials (Templates partiels)
Les partials permettent de reutiliser des portions de templates.
Enregistrement des partials
// Dans le hook init ou une fonction dediee
async function preloadHandlebarsTemplates() {
const partials = [
"systems/mon-systeme/templates/partials/ability-block.hbs",
"systems/mon-systeme/templates/partials/item-row.hbs",
"systems/mon-systeme/templates/partials/resource-bar.hbs"
];
return loadTemplates(partials);
}
// Appel dans init
Hooks.once("init", async function() {
await preloadHandlebarsTemplates();
});
Utilisation des partials
<!-- Inclusion simple -->
{{> "systems/mon-systeme/templates/partials/ability-block.hbs"}}
<!-- Avec passage de contexte -->
{{> "systems/mon-systeme/templates/partials/item-row.hbs" item=weapon editable=editable}}
<!-- Dans une boucle -->
{{#each items}}
{{> "systems/mon-systeme/templates/partials/item-row.hbs" item=this}}
{{/each}}
Exemple de partial
<!-- templates/partials/ability-block.hbs -->
<div class="ability-block" data-ability="{{ability.key}}">
<label class="ability-label">
{{localize ability.label}}
</label>
<input type="number"
name="system.abilities.{{ability.key}}.value"
value="{{ability.value}}"
{{#unless editable}}disabled{{/unless}}>
<button type="button"
class="roll-ability"
data-action="rollAbility"
data-ability="{{ability.key}}">
{{numberSign ability.mod}}
</button>
</div>
Organisation des templates
Structure recommandee
templates/
+-- actor/
| +-- character-sheet.hbs # Template principal
| +-- npc-sheet.hbs
| +-- parts/ # PARTS pour ApplicationV2
| +-- header.hbs
| +-- attributes.hbs
| +-- inventory.hbs
| +-- spells.hbs
|
+-- item/
| +-- item-sheet.hbs
| +-- parts/
| +-- header.hbs
| +-- details.hbs
|
+-- partials/ # Composants reutilisables
| +-- ability-block.hbs
| +-- item-row.hbs
| +-- resource-bar.hbs
| +-- roll-button.hbs
|
+-- chat/ # Messages de chat
| +-- roll-card.hbs
| +-- item-card.hbs
|
+-- dialog/ # Boites de dialogue
+-- roll-dialog.hbs
+-- rest-dialog.hbs
Bonnes pratiques
- Un template = une responsabilite : Evitez les templates trop longs
- Utilisez les partials : Pour tout composant reutilise plus d'une fois
- Prefixez les classes CSS :
.mon-systeme-abilitypour eviter les conflits - Gardez la logique minimale : Preparez les donnees dans
_prepareContext()
Utilisez l'extension .hbs pour les fichiers Handlebars.
Certains editeurs (comme VS Code) offrent une coloration syntaxique specifique.
Helpers personnalises
Vous pouvez creer vos propres helpers pour des besoins specifiques.
Enregistrement d'un helper
// Dans le hook init
Hooks.once("init", function() {
// Helper simple : retourne une valeur
Handlebars.registerHelper("modFromScore", function(score) {
return Math.floor((score - 10) / 2);
});
// Helper avec signe (+/-)
Handlebars.registerHelper("signedMod", function(score) {
const mod = Math.floor((score - 10) / 2);
return mod >= 0 ? `+${mod}` : String(mod);
});
// Helper conditionnel (bloc)
Handlebars.registerHelper("ifGte", function(a, b, options) {
if (a >= b) {
return options.fn(this); // Contenu du bloc
}
return options.inverse(this); // Contenu du else
});
// Helper avec plusieurs arguments
Handlebars.registerHelper("damageFormula", function(baseDice, mod, options) {
const sign = mod >= 0 ? "+" : "";
return `${baseDice}${sign}${mod}`;
});
// Helper retournant du HTML (SafeString)
Handlebars.registerHelper("abilityIcon", function(ability) {
const icons = {
str: "fa-fist-raised",
dex: "fa-running",
con: "fa-heart",
int: "fa-brain",
wis: "fa-eye",
cha: "fa-comments"
};
const icon = icons[ability] || "fa-question";
return new Handlebars.SafeString(``);
});
});
Utilisation des helpers personnalises
<!-- Helper simple -->
<span class="modifier">{{signedMod system.abilities.str.value}}</span>
<!-- Helper bloc -->
{{#ifGte system.level 5}}
<span class="feature">Attaque Supplementaire</span>
{{else}}
<span class="locked">Disponible au niveau 5</span>
{{/ifGte}}
<!-- Helper avec plusieurs arguments -->
<span>Degats: {{damageFormula "2d6" system.abilities.str.mod}}</span>
<!-- Helper avec HTML -->
{{{abilityIcon "str"}}}
Exercice pratique : Creer un template de fiche
Creons un template complet pour une fiche de personnage.
1. Template principal
<!-- templates/actor/character-sheet.hbs -->
<form class="{{cssClass}}" autocomplete="off">
{{!-- En-tete avec nom et image --}}
<header class="sheet-header">
<img class="profile-img" src="{{actor.img}}"
data-edit="img" title="{{actor.name}}">
<div class="header-fields">
<h1 class="charname">
<input type="text" name="name" value="{{actor.name}}"
placeholder="{{localize 'MON_SYSTEME.Name'}}">
</h1>
<div class="resources">
{{> "systems/mon-systeme/templates/partials/resource-bar.hbs"
resource=system.hp
name="hp"
label="MON_SYSTEME.HP"}}
</div>
</div>
</header>
{{!-- Navigation par onglets --}}
<nav class="sheet-tabs">
<a class="item {{#if tabs.attributes.active}}active{{/if}}"
data-tab="attributes">
{{localize "MON_SYSTEME.Tabs.Attributes"}}
</a>
<a class="item {{#if tabs.inventory.active}}active{{/if}}"
data-tab="inventory">
{{localize "MON_SYSTEME.Tabs.Inventory"}}
</a>
<a class="item {{#if tabs.biography.active}}active{{/if}}"
data-tab="biography">
{{localize "MON_SYSTEME.Tabs.Biography"}}
</a>
</nav>
{{!-- Contenu des onglets --}}
<section class="sheet-body">
{{!-- Onglet Attributs --}}
<div class="tab {{#if tabs.attributes.active}}active{{/if}}"
data-group="primary" data-tab="attributes">
<section class="abilities-grid">
{{#each abilities}}
{{> "systems/mon-systeme/templates/partials/ability-block.hbs"
ability=this
editable=../editable}}
{{/each}}
</section>
<section class="skills-list">
<h2>{{localize "MON_SYSTEME.Skills"}}</h2>
{{#each skills}}
<div class="skill-row">
<input type="checkbox"
name="system.skills.{{@key}}.proficient"
{{checked this.proficient}}>
<span class="skill-name">{{this.label}}</span>
<button type="button"
data-action="rollSkill"
data-skill="{{@key}}">
{{numberSign this.total}}
</button>
</div>
{{/each}}
</section>
</div>
{{!-- Onglet Inventaire --}}
<div class="tab {{#if tabs.inventory.active}}active{{/if}}"
data-group="primary" data-tab="inventory">
<header class="inventory-header">
<h2>{{localize "MON_SYSTEME.Inventory"}}</h2>
<button type="button" data-action="createItem">
<i class="fas fa-plus"></i> {{localize "MON_SYSTEME.AddItem"}}
</button>
</header>
{{#if (gt items.length 0)}}
<ol class="items-list">
{{#each items}}
{{> "systems/mon-systeme/templates/partials/item-row.hbs"
item=this
editable=../editable}}
{{/each}}
</ol>
{{else}}
<p class="empty-message">
{{localize "MON_SYSTEME.NoItems"}}
</p>
{{/if}}
</div>
{{!-- Onglet Biographie --}}
<div class="tab {{#if tabs.biography.active}}active{{/if}}"
data-group="primary" data-tab="biography">
<div class="editor-container">
{{editor content=enriched.biography
target="system.biography"
button=true
editable=editable}}
</div>
</div>
</section>
</form>
2. Partial ability-block.hbs
<!-- templates/partials/ability-block.hbs -->
<div class="ability {{#if ability.highlighted}}highlighted{{/if}}">
<h3 class="ability-name">
{{{abilityIcon ability.key}}}
{{ability.label}}
</h3>
<div class="ability-score">
<input type="number"
name="system.abilities.{{ability.key}}.value"
value="{{ability.value}}"
min="1" max="30"
{{#unless editable}}disabled{{/unless}}>
</div>
<button type="button"
class="ability-mod rollable"
data-action="rollAbility"
data-ability="{{ability.key}}"
{{#unless editable}}disabled{{/unless}}>
{{signedMod ability.value}}
</button>
</div>
3. Partial item-row.hbs
<!-- templates/partials/item-row.hbs -->
<li class="item-row" data-item-id="{{item.id}}">
<img class="item-img" src="{{item.img}}" alt="{{item.name}}">
<h4 class="item-name">
<a data-action="viewItem" data-item-id="{{item.id}}">
{{item.name}}
</a>
</h4>
{{#if item.system.quantity}}
<span class="item-quantity">x{{item.system.quantity}}</span>
{{/if}}
{{#if editable}}
<div class="item-controls">
<a data-action="editItem" data-item-id="{{item.id}}" title="Modifier">
<i class="fas fa-edit"></i>
</a>
<a data-action="deleteItem" data-item-id="{{item.id}}" title="Supprimer">
<i class="fas fa-trash"></i>
</a>
</div>
{{/if}}
</li>
4. Partial resource-bar.hbs
<!-- templates/partials/resource-bar.hbs -->
<div class="resource-bar" data-resource="{{name}}">
<label>{{localize label}}</label>
<div class="bar-container">
<div class="bar-fill"
style="width: {{percentage resource.value resource.max}}%">
</div>
<div class="bar-values">
<input type="number"
name="system.{{name}}.value"
value="{{resource.value}}"
min="0" max="{{resource.max}}">
<span class="separator">/</span>
<input type="number"
name="system.{{name}}.max"
value="{{resource.max}}"
min="1">
</div>
</div>
</div>
Vous avez cree un ensemble complet de templates modulaires et reutilisables. Cette structure vous servira de base pour des fiches plus complexes.