HTMX et Django
Bonjour,
J'ai envie de me mettre un peu à HTMX. Pour mon dernier projet c'est indispensable.
Je sais que Thibault et Vincent vous êtes des pro de HTMX ^^
Dans mon projet rpg, lorsqu'on attaque je voudrais ne recharger que la partie du perso et de l'ennemi.
J'ai donc utilise hx-post avec hx-target et crée une div qui englobe les deux colonnes : celle du perso et celle de l'ennemi.
Cependant, comme vous pouvez le voir dans la vidéo ci-dessous j'ai mon écran qui est doublé. Comme si ce qui était hors de la div où pointe hx-target était doublé (en haut de la page et aussi en bas de la page).
Vidéo ici ==> LIEN
L'action attaque fait le boulot en enlevant les points et en chargeant les messages et redirige vers la vue branche avec le perso, l'ennemi, l'inventaire etc.
Est-ce qu'il y a des conditions à respecter avec htmx au niveau des vues de django ?
Merci d'avance
Alors voici le lien git vers le html. J'ai crée une branche git à part htmx-test.
https://github.com/gabigab117/webrpg/blob/htmx_test/project/rpg/templates/rpg/branch.html
Ligne 100 tu as le bouton attaquer avec les balises hx.
La div ciblée commence ligne 42 pour se terminer ligne 86.
Merci d'avance :)
Salut Gab !
Alors le problème c'est qu'effectivement tu retournes toute la branche : dans ta vue attack, tu effectues un redirect vers la vue branch.
Et dans la vue branch tu retournes ton fichier branch.html donc c'est tout ce fichier qui se retrouve dans ton hx-target :
return render(request, "rpg/branch.html", context={"branch": branch, "choices": choices,
"game_character": game_character,
"enemy": enemy,
"story": story,
"inventory": inventory})
Et dans le fichier branch.html c'est la page complète :
{% extends 'base.html' %}
{% block title %}{{ branch.name }}{% endblock %}
{% block body %}
<div class="container" style="margin-top: 80px;">
<div class="text-center">
<h2>{{ branch.name }}</h2>
</div>
<div class="row">
<div class="col">
<div class="text-center">
<img class="rounded" height="auto" src="{{ branch.illustration.illustration.url }}" width="400"/>
</div>
<p style="margin-top: 30px;">
{{ branch.script_1|safe }}
</p>
</div>
</div>
<div class="row">
<div class="col">
...
Avec HTMX, il faut vraiment retourner uniquement le morceau de HTML que tu souhaites inclure. Ça nécessite de travailler avec des plus petits composants.
Tu peux faire ça de différentes façons, je vais te donner un exemple un peu plus concret.
Tu peux effectivement utiliser ta même vue et utiliser le header "Hx-Request" qui est envoyé par HTMX pour savoir quand la requête vient de HTMX ou non :
# urls.py
path('fight/', views.fight, name='fight'),
# views.py
def fight(request):
if request.headers.get("Hx-Request"):
# On est dans le cas d'une attaque du joueur
# On calcule les dégâts infligés et on retourne juste le morceau de template qui indique les dégâts infligés
damage = user.attack(request.POST.get('enemy'))
return render(request, 'fight/damage.html', context={'damage': damage})
# Si le headers ne contient pas Hx-Request, il ne s'agit pas d'une requête HTMX, on considère donc qu'on affiche juste la page complète du combat
user_life = request.user.get_health()
ennemy_life = get_ennemy_health()
return render(request, 'fight/index.html', context={'user_life': user_life, 'ennemy_life': ennemy_life})
Et dans tes templates :
<!-- fight/index.html -->
<body>
<h1>Combat en cours</h1>
<div>Tes poinst de vie</div>
<p>{{ user_life }}</p>
<p>Points de vie de l'ennemi : {{ ennemy_life }}</p>
<div id="attack-result"></div>
<button hx-get="{% url 'fight' %}" hx-target="#attack-result">Attaquer</button>
</body>
Et dans le template que tu retournes avec HTMX :
<!-- fight/damage.html -->
<span>Tu as infligé {{ damage }} dommages à l'ennemi !</span>
Ça c'est une première façon de faire avec une seule vue et une seule URL pour gérer les deux cas de figure.
Ça marche et ça évite de créer deux vues et deux URL mais tu commences à avoir pas mal de logique au même endroit. Généralement c'est mieux de séparer les choses.
Pour ça il suffit de créer deux vues et deux URL :
# urls.py
path('fight/', views.fight, name='fight'),
path('attack/', views.attack, name='attack'),
# views.py
def fight(request):
"""Un combat est en cours, on affiche la page du combat"""
user_life = request.user.get_health()
ennemy_life = get_ennemy_health()
return render(request, 'fight/index.html', context={'user_life': user_life, 'ennemy_life': ennemy_life})
def attack(request): # HTMX
"""Attaque du joueur : on calcule les dégâts et on les retourne dans la page"""
damage = user.attack(request.POST.get('enemy'))
return render(request, 'fight/damage.html', context={'damage': damage})
Et bien sûr dans ton HTML tu changes le bouton :
<button hx-get='{% url "attack" %}' hx-target="#attack-result">Attaquer</button>
Tout ça est très schématique avec des noms de fonction imaginaire mais c'est juste pour illustrer bien sûr ;)
Re,
Alors, comme j'avais déjà deux vues, j'ai pris la solution des deux vues.
J'y sui presque ! lol
Regardes la vidéo, tout se passe bien jusqu'au redirect ou là ça me double encore tout. Pourtant c'est un redirect, je ne pensais pas que j'aurais encore ce problème.
https://drive.google.com/file/d/1ZG5Xue_NbXdxBiBNMgaVViR9z48dwACb/view?usp=drive_link
La vue branch où j'affiche tout : le fil de l'histoire avec le perso, l'ennemi le bouton attaquer etc....
@login_required()
def branch_view(request, pk, story_id):
user = request.user
game_character = get_object_or_404(CharacterUser, pk=pk)
inventory = game_character.inventory.items_in.all()
story = Story.objects.get(story_id=story_id)
if request.method == "POST":
# Création de la sauvegarde temporaire
StoryCompleted.objects.get_or_create(story=story, character=game_character)
story_completed = StoryCompleted.objects.get(story=story, character=game_character)
branch = Branch.objects.get(digit=story_completed.sum_branches_digit, story=story)
branch_enemy = branch.enemy
choices = branch.choices.all()
# Si j'ai un ennemi
if branch_enemy:
# Si pas d'ennemi en BDD je le crée
if not EnemyUser.objects.filter(user=user, name=branch_enemy.name).exists():
if request.method == "POST":
enemy_user, _ = EnemyUser.objects.get_or_create(
user=user,
style=branch_enemy.style,
name=branch_enemy.name,
life=branch_enemy.life,
attack=branch_enemy.attack,
shield=branch_enemy.shield,
avatar=branch_enemy.avatar
)
try:
enemy = EnemyUser.objects.get(user=user)
except ObjectDoesNotExist:
enemy = None
return render(request, "rpg/branch/branch.html", context={"branch": branch, "choices": choices,
"game_character": game_character,
"enemy": enemy, "story": story, "inventory": inventory})
Si je clique sur Attaquer autre vue :
def enemy_attack_view(request, pk, story_id):
user = request.user
story = Story.objects.get(story_id=story_id)
enemy = EnemyUser.objects.get(pk=pk)
game_character = CharacterUser.objects.get(user=user, story=story)
inventory = Inventory.objects.get(story=story, character_user=game_character)
money_user = MoneyUser.objects.get(character_user=game_character)
if request.method == "POST":
game_character.action_attack(request=request, enemy=enemy)
if enemy.life <= 0:
enemy.delete()
inventory.random_items(request=request, story=story, character_user=game_character)
money_user.add_money(request, character_user=game_character)
return redirect("rpg:branch", pk=game_character.pk, story_id=story.story_id)
if enemy.life > 0:
enemy.action_attack(request=request, game_character=game_character)
if game_character.life <= 0:
game_character.delete()
enemy.delete()
storage = messages.get_messages(request)
for _ in storage:
pass
return redirect("rpg:game-over")
return render(request, "rpg/branch/fight.html", context={"game_character": game_character, "enemy": enemy})
Du coup dans la vue branch je render mon html :
{% extends 'base.html' %}
{% block title %}{{ branch.name }}{% endblock %}
{% block body %}
<div class="container" style="margin-top: 80px;">
<div class="text-center">
<h2>{{ branch.name }}</h2>
</div>
<div class="row">
<div class="col">
<div class="text-center">
<img class="rounded" height="auto" src="{{ branch.illustration.illustration.url }}" width="400"/>
</div>
<p style="margin-top: 30px;">
{{ branch.script_1|safe }}
</p>
</div>
</div>
<div class="row">
<div class="col">
<p style="margin-top: 30px;">
{{ branch.script_2|safe }}
</p>
</div>
</div>
<div class="row">
<div class="col">
<p style="margin-top: 30px;">
{{ branch.script_3|safe }}
</p>
</div>
</div>
<div class="row">
<div class="col">
<p style="margin-top: 30px;">
{{ branch.script_4|safe }}
</p>
</div>
</div>
{% include 'rpg/branch/fight.html' %}
<div class="row"><div class="col">
{% if enemy %}
<div class="text-center" style="margin-bottom: 20px; margin-top: 20px;">
<form>
{% csrf_token %}
<button class="btn btn-success" hx-post="{% url 'rpg:attack' enemy.pk story.story_id %}" hx-target="#characters">Attaquer</button>
</form>
</div>
{% endif %}
</div>
</div>
<div class="row"><div class="text-center" style="margin-bottom: 20px; margin-top: 20px;">
<h3>Inventaire</h3>
</div>
{% if inventory %}{% for item in inventory %}
<div class="col"><div class="text-center" style="margin-bottom: 20px; margin-top: 20px;">
<form action="{% url 'rpg:get-item' item.pk story.story_id %}" method="post">
{% csrf_token %}
<input alt="image bouton" class="rounded" height="auto" src="{{ item.item.illustrations.illustration.url }}" type="image" width="75"/>
</form>
{{ item.item.name }} - {{ item.quantity }}({{ item.item.feature }})
</div>
</div>
{% endfor %}{% else %}
<div class="col"><div class="text-center" style="margin-bottom: 20px; margin-top: 20px;">
<h3>Inventaire vide.</h3>
</div>
</div>
{% endif %}
</div>
<div class="row"><div class="col">
<img class="rounded" height="auto" src="{{ game_character.moneyuser.money.illustration.illustration.url }}" width="70"/>
<br/>
{{ game_character.moneyuser.quantity }} - {{ game_character.moneyuser.money.name }}
</div>
</div>
</div>
<!-- abandonner -->
{% if enemy %}
<form action="{% url 'rpg:give-up' game_character.pk enemy.id %}" method="post">{% csrf_token %}
<button class="btn btn-danger btn-sm">Abandonner
</button>
</form>
{% endif %}
{% endblock %}
Avec un include fight.html :
<div id="characters">
<div class="row">
<div class="col-6">
<div class="text-center" style="margin-bottom: 20px;">
<img class="rounded" height="auto" src="{{ game_character.avatar.illustration.url }}" width="200"/>
</div>
<div class="text-center" style="margin-bottom: 20px;">
{{ game_character.user }} - {{ game_character.name }} - {{ game_character.style }}
</div>
<!-- Caractéristiques-->
<ul class="list-group custom-list-group">
<li class="list-group-item d-flex justify-content-between align-items-center">Vie<span class="badge bg-primary rounded-pill">{{ game_character.life }}</span></li>
<li class="list-group-item d-flex justify-content-between align-items-center">Attaque<span class="badge bg-primary rounded-pill">{{ game_character.attack }}</span></li>
<li class="list-group-item d-flex justify-content-between align-items-center">Constitution<span class="badge bg-primary rounded-pill">{{ game_character.constitution }}</span></li>
<li class="list-group-item d-flex justify-content-between align-items-center">Charisme<span class="badge bg-primary rounded-pill">{{ game_character.charisma }}</span></li>
<li class="list-group-item d-flex justify-content-between align-items-center">Chance<span class="badge bg-primary rounded-pill">{{ game_character.chance }}</span></li>
<li class="list-group-item d-flex justify-content-between align-items-center">Magie<span class="badge bg-primary rounded-pill">{{ game_character.magic }}</span></li>
<li class="list-group-item d-flex justify-content-between align-items-center">Bouclier<span class="badge bg-primary rounded-pill">{{ game_character.shield }}</span></li>
</ul>
</div>
<!--Ennemi -->
<div class="col-6">
{% if enemy %}
<div class="text-center" style="margin-bottom: 20px;">
<img class="rounded" height="auto" src="{{ enemy.avatar.illustration.url }}" width="200"/>
</div>
<div class="text-center" style="margin-bottom: 20px;">
{{ enemy.name }} - {{ enemy.style }}
</div>
<!-- Caractéristiques-->
<ul class="list-group custom-list-group">
<li class="list-group-item d-flex justify-content-between align-items-center">Vie<span class="badge bg-primary rounded-pill">{{ enemy.life }}</span></li>
<li class="list-group-item d-flex justify-content-between align-items-center">Attaque<span class="badge bg-primary rounded-pill">{{ enemy.attack }}</span></li>
<li class="list-group-item d-flex justify-content-between align-items-center">Bouclier<span class="badge bg-primary rounded-pill">{{ enemy.shield }}</span></li>
</ul>
{% else %}
<!-- Si pas d'ennemi je fais un choix -->{% if choices %}{% for choice in choices %}<div class="text-center" style="margin-bottom: 20px; margin-top: 20px;"><form action="{% url 'rpg:next-branch' choice.pk story.story_id %}" method="post">{% csrf_token %}<button class="btn btn-success">{{ choice.name }}</button></form></div>{% endfor %}{% else %}<div class="text-center" style="margin-bottom: 20px; margin-top: 20px;"><form action="{% url 'rpg:the-end' story.story_id %}" method="post">{% csrf_token %}<button class="btn btn-success">Fin de l'histoire</button>
</form>
</div>
{% endif %}{% endif %}
</div>
</div>
<div class="row"><div class="col">
<div class="text-center" style="margin-bottom: 20px; margin-top: 20px;">
{% if messages %}{% for message in messages %}
<h4 style="color: red;">{{ message }}</h4>
{% endfor %}{% endif %}
</div>
</div>
</div>
</div>
Comme ça quand j'attaque je ne retourne qu'un morceau de code.
Mais alors dès que mon perso est ko ou l'ennemi c'est un redirect et là tout est doublé lol
EDIT 21/8
Du coup :
si l'enemi est KO je fais un redirect sur la vue attaque pour rafraichir que la portion de code(fight.html) pour ne pas tout doubler.
Par contre pour le redirect vers game-over, je rafraichi que la vue attaque en affichant game over si le personnage joueur est ko ? en gardant la vue attaque comme ça je rafraichi toujours que le fight.html ? Et je supprime la vue game-over.html
La nuit m'a peut-être portée conseil lol.
J'essaye ça ce soir
Bon par rapport à mon message juste ci-dessus je n'ai pas avancé.
J'ai essayé, mais plus ça va plus je m'enfonce...
En fait avec un simple formulaire, HTMX j'ai bien compris.
Mais là le truc c'est que j'attaque, les deux perdent de la vie, mais si l'ennemi est KO c'est d'autres boutons qui s'affichent normalement.
https://github.com/gabigab117/webrpg/blob/htmx_test/project/rpg/views.py
En fait la vue avec la partie de l'histoire où on est, avec les deux personnages qui se font faces c'est branch_view ligne 89.
Quand tu attaques ça appel vue enemy_attack_view ligne 150.
Quand l'ennemi est KO, donc ligne 165 je refais un redirect donc forcément ça double. j'ai essayé avec un render mais ça n'allait pas.
EDIT :
https://drive.google.com/file/d/1VdwvFYDO5MRlYFIhkj-_dH1BFmeD_nK6/view?usp=sharing
Regarde ce que j'ai fait en JavaScript lol. J'ai juste eu une balise à ajouter et je ne modifie pas les vues d'origine.
Une balise html apparait quand une premiere attaque est exécutée
Tu en penses quoi ?
En fait je pense que le mieux c'est de t'entraîner avec HTMX sur un projet plus simple.
Là commencer directement avec ce projet tu risques de passer trop de temps à déboguer des problèmes qui peuvent venir de plein d'endroits différents (un peu comme avec ton autre question sur le test unitaire, avec le captcha, vérification email, pour s'entraîner sur les tests unitaires, ça fait beaucoup de variables).
Ça te permettra de maîtriser ça sur un projet simple pour bien comprendre les mécaniques de HTMX avec Django et revenir plus sereinement dessus par la suite dans ton projet :)
En fait HTMX ce n'est rien d'autre que du JavaScript derrière ;) Tu peux lire cet article pour voir les requêtes avec Fetch en JS directement :
https://www.docstring.fr/blog/les-requetes-ajax-avec-django/
Inscris-toi
(c'est gratuit !)
Tu dois créer un compte pour participer aux discussions.
Créer un compte