Cours Vue.js · Module 2: Syntaxe des templates

Les directives Vue

6 min de lecture
Dans cette leçon4

Les directives sont des attributs spéciaux commençant par v-. Chaque directive est comme une instruction pour le système de contrôle de la base martienne.

Les moustaches de la leçon précédente ont deux lacunes. Elles n'atteignent pas les attributs, donc l'adresse de l'image d'un capteur ou le blocage d'un bouton ne peuvent pas dépendre des données. Et elles n'entendent pas l'opérateur : un clic sur l'hologramme ne déclenche rien. Les directives comblent ces deux lacunes. Leur structure est toujours la même : un nom après le préfixe v-, un argument après les deux-points, des modificateurs après des points et une expression entre guillemets. Dans v-on:submit.prevent="onSubmit", le nom est on, l'argument submit, le modificateur prevent et la valeur la fonction onSubmit.

v-bind - Liaison d'attributs

v-bind garde un attribut synchronisé avec les données : après les deux-points tu indiques le nom de l'attribut, et entre guillemets une expression. Comme cette directive revient sans arrêt, elle a un raccourci - les deux-points seuls. Voici le template d'un panneau avec quatre cas typiques :

1<template>
2  <div class="control-panel" style="background: #0a0e27; padding: 1rem;">
3    <!-- Syntaxe complète -->
4    <img v-bind:src="sensorImage" v-bind:alt="sensorName">
5
6    <!-- Syntaxe raccourcie (recommandée) -->
7    <img :src="sensorImage" :alt="sensorName">
8
9    <!-- Attributs dynamiques -->
10    <a :href="documentationUrl" :class="linkClass">Documentation</a>
11
12    <!-- Attributs booléens -->
13    <button :disabled="isProcessing" class="btn-neon">Launch</button>
14  </div>
15</template>

Les deux images produisent un HTML identique - le raccourci ne change que la longueur de l'écriture. Entre les guillemets se trouve une expression, pas du texte : :alt="sensorName" insère la valeur de la variable, alors qu'un simple alt="sensorName" écrirait littéralement le mot sensorName. L'attribut booléen disabled apparaît quand la valeur est vraie et disparaît pour false. Quand une liaison donne null ou undefined, Vue retire complètement l'attribut.

Les données de ces liaisons sont de simples refs de la section script - rien de nouveau par rapport au module 1 :

1<script setup>
2import { ref } from 'vue'
3
4const sensorImage = ref('/images/oxygen-sensor.png')
5const sensorName = ref('Oxygen Sensor MK-7')
6const documentationUrl = ref('https://nova-lab.mars/docs')
7const linkClass = ref('neon-link')
8const isProcessing = ref(false)
9</script>

Il suffit de régler isProcessing.value = true pour que le bouton reçoive l'attribut disabled - tu ne touches pas au template. L'apparence du bouton vient d'un CSS ordinaire, car v-bind ne change pas le fonctionnement des styles :

1<style>
2.btn-neon {
3  background: transparent;
4  border: 2px solid #00ff88;
5  color: #00ff88;
6  padding: 0.5rem 1rem;
7  cursor: pointer;
8}
9.btn-neon:hover {
10  background: #00ff8822;
11  box-shadow: 0 0 10px #00ff88;
12}
13</style>

Il n'y a pas un seul élément propre à Vue dans la section des styles. Mon conseil : écris toujours le raccourci : - dans un template dense, tu vois tout de suite quels attributs sont vivants. Depuis Vue 3.4, une forme encore plus courte fonctionne : quand l'attribut et la variable portent le même nom, :id signifie la même chose que :id="id".

v-on - Gestion des événements

v-on attache une fonction à un événement du navigateur ; son raccourci est l'arobase @. La valeur peut être un nom de fonction ou une courte instruction, par exemple un appel avec un argument. La variable spéciale $event transmet l'objet événement quand tu appelles la fonction toi-même :

1<template>
2  <div class="reactor-controls" style="background: #0a0e27; color: #00b4d8;">
3    <!-- Syntaxe complète -->
4    <button v-on:click="handleActivation">Activate</button>
5
6    <!-- Syntaxe raccourcie (recommandée) -->
7    <button @click="handleActivation" class="btn-reactor">Activate Reactor</button>
8
9    <!-- Gestionnaire inline -->
10    <button @click="powerLevel++">Increase Power</button>
11
12    <!-- Avec argument -->
13    <button @click="sendSignal('ALPHA-BASE')">Send Signal</button>
14
15    <!-- Avec l'objet événement -->
16    <button @click="handleActivation($event)">Event Info</button>
17  </div>
18</template>

Le compilateur reconnaît ce que tu lui donnes : le nom seul handleActivation signifie que Vue appelle la fonction et lui passe l'événement natif comme unique argument. Une écriture avec parenthèses est une instruction, donc l'événement doit être transmis explicitement par $event. Il n'y a pas de .value dans powerLevel++, car les refs sont déballés dans le template.

Les fonctions de gestion vivent dans le script, où .value s'applique de nouveau :

1<script setup>
2import { ref } from 'vue'
3
4const powerLevel = ref(0)
5
6function handleActivation(event) {
7  console.log('Reactor activated!', event)
8}
9
10function sendSignal(baseCode) {
11  alert(`Sending signal to: ${baseCode}`)
12}
13</script>

Le bouton reste un <button> ordinaire - v-on ne fait qu'y ajouter une écoute d'événement, comme le ferait addEventListener.

Modificateurs d'événement

Les appels comme event.preventDefault() ou event.stopPropagation() encombrent les fonctions de détails du DOM. Un modificateur ajouté après un point règle cela dans le template. L'aide-mémoire ci-dessous ne montre que les balises ouvrantes :

1<template>
2  <div class="mission-control">
3    <!-- Empêcher l'action par défaut -->
4    <form @submit.prevent="onSubmit" class="mission-form">
5
6    <!-- Arrêter la propagation -->
7    <div @click.stop="onClick" class="stop-zone">
8
9    <!-- Une seule fois -->
10    <button @click.once="launchSequence" class="btn-launch">Start Sequence</button>
11
12    <!-- Mode capture -->
13    <div @click.capture="onCapture" class="capture-zone">
14
15    <!-- Uniquement soi-même -->
16    <div @click.self="onSelf" class="self-zone">
17
18    <!-- Combinaison -->
19    <a @click.stop.prevent="onClick" class="safe-link">
20  </div>
21</template>

.prevent appelle event.preventDefault(), donc le formulaire ne recharge pas la page. .stop appelle event.stopPropagation(), et l'événement n'atteint pas les parents. .once ne laisse le gestionnaire agir qu'une seule fois, .capture traite l'événement en phase de capture, avant qu'il n'atteigne l'enfant, et .self ne réagit que si l'élément lui-même a été cliqué. Quand l'événement arrive, Vue applique d'abord les modificateurs, puis seulement appelle ta fonction. Dans une chaîne, l'ordre compte : @click.prevent.self bloque l'action par défaut aussi pour les enfants, tandis que @click.self.prevent ne le fait que pour l'élément lui-même.

Modificateurs de touches

Les événements keyup et keydown ont eux aussi des modificateurs. Vue connaît les alias .enter, .tab, .delete (Suppr et Retour arrière), .esc, .space et les flèches, et les touches système sont .ctrl, .alt, .shift et .meta. Les fonctions de l'exemple sont définies dans le script :

1<template>
2  <input @keyup.enter="sendCommand" placeholder="Commande pour la station">
3  <input @keyup.esc="clearCommand" placeholder="Échap vide le champ">
4  <button @click.ctrl="emergencyStop">Ctrl + clic : arrêt d'urgence</button>
5</template>

sendCommand ne part qu'après Entrée, clearCommand qu'après Échap, et l'arrêt d'urgence exige la touche Ctrl enfoncée. Les autres touches ne déclenchent rien - sans un seul if qui vérifie event.key. Dans la prochaine leçon, tu verras que :class et :style sont des variantes spéciales de v-bind qui acceptent des objets et des tableaux.

Retiens : les deux-points relient un attribut aux données, l'arobase relie un événement à une fonction, et le point ajoute à l'ordre un protocole de sécurité.

Code de cette leçon : App.vue
1<script setup>
2import { ref } from 'vue'
3
4const stationStatus = ref('operational')
5const powerLevel = ref(85)
6const alertLevel = ref('low')
7const showDetails = ref(false)
8const logoUrl = ref('https://placehold.co/100/00ff88/0a0e27?text=NOVA')
9
10const systemMessages = ref([
11  { id: 1, text: 'Systèmes de survie : EN LIGNE', type: 'success' },
12  { id: 2, text: 'Niveau d’énergie : STABLE', type: 'info' },
13  { id: 3, text: 'Communication avec la Terre : ACTIVE', type: 'success' }
14])
15
16function toggleDetails() {
17  showDetails.value = !showDetails.value
18}
19
20function increasePower() {
21  if (powerLevel.value < 100) {
22    powerLevel.value += 5
23  }
24}
25
26function decreasePower() {
27  if (powerLevel.value > 0) {
28    powerLevel.value -= 5
29  }
30}
31</script>
32
33<template>
34  <div class="control-panel">
35    <div class="station-header">
36      <!-- v-bind pour les attributs src et alt -->
37      <img :src="logoUrl" :alt="'NOVA LAB Logo'" class="logo">
38      <h1>Station de recherche NOVA</h1>
39    </div>
40
41    <div class="power-control">
42      <h2>Contrôle de l'énergie</h2>
43      <!-- v-bind pour class et style -->
44      <div
45        class="power-bar"
46        :class="{ 'critical': powerLevel < 30, 'optimal': powerLevel >= 70 }"
47        :style="{ width: powerLevel + '%' }"
48      >
49        {{ powerLevel }}%
50      </div>
51
52      <!-- v-on (@) pour la gestion des événements -->
53      <div class="controls">
54        <button @click="decreasePower" class="btn btn-danger">-5%</button>
55        <button @click="increasePower" class="btn btn-success">+5%</button>
56      </div>
57    </div>
58
59    <div class="status-section">
60      <h2>Statut des systèmes</h2>
61      <button @click="toggleDetails" class="btn btn-info">
62        {{ showDetails ? 'Masquer les détails' : 'Afficher les détails' }}
63      </button>
64
65      <div v-show="showDetails" class="messages">
66        <div
67          v-for="msg in systemMessages"
68          :key="msg.id"
69          :class="['message', 'message-' + msg.type]"
70        >
71          {{ msg.text }}
72        </div>
73      </div>
74    </div>
75
76    <div class="info-box">
77      <h3>Les directives Vue.js</h3>
78      <ul>
79        <li><strong>v-bind</strong> (raccourci :) - liaison d'attributs</li>
80        <li><strong>v-on</strong> (raccourci @) - gestion des événements</li>
81        <li><strong>:class</strong> - classes CSS dynamiques</li>
82        <li><strong>:style</strong> - styles inline dynamiques</li>
83      </ul>
84    </div>
85  </div>
86</template>
87
88<style scoped>
89.control-panel {
90  background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
91  min-height: 100vh;
92  padding: 2rem;
93  color: #ffffff;
94  font-family: 'Courier New', monospace;
95}
96
97.station-header {
98  text-align: center;
99  margin-bottom: 2rem;
100}
101
102.logo {
103  width: 100px;
104  height: 100px;
105  border-radius: 50%;
106  border: 3px solid #00ff88;
107  box-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
108}
109
110h1, h2, h3 {
111  color: #00ff88;
112  text-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
113}
114
115.power-control {
116  background: rgba(0, 255, 136, 0.1);
117  border: 2px solid #00ff88;
118  border-radius: 10px;
119  padding: 1.5rem;
120  margin-bottom: 2rem;
121}
122
123.power-bar {
124  height: 40px;
125  background: linear-gradient(90deg, #00ff88, #00b4d8);
126  border-radius: 20px;
127  display: flex;
128  align-items: center;
129  justify-content: center;
130  font-weight: bold;
131  transition: all 0.3s ease;
132  margin: 1rem 0;
133}
134
135.power-bar.critical {
136  background: linear-gradient(90deg, #ff0055, #ff6600);
137  animation: pulse 1s infinite;
138}
139
140.power-bar.optimal {
141  box-shadow: 0 0 20px rgba(0, 255, 136, 0.8);
142}
143
144@keyframes pulse {
145  0%, 100% { opacity: 1; }
146  50% { opacity: 0.7; }
147}
148
149.controls {
150  display: flex;
151  gap: 1rem;
152  justify-content: center;
153  margin-top: 1rem;
154}
155
156.btn {
157  padding: 0.75rem 1.5rem;
158  border: none;
159  border-radius: 5px;
160  font-size: 1rem;
161  font-weight: bold;
162  cursor: pointer;
163  transition: all 0.3s ease;
164  font-family: 'Courier New', monospace;
165}
166
167.btn-success {
168  background: #00ff88;
169  color: #0a0e27;
170}
171
172.btn-danger {
173  background: #ff0055;
174  color: #ffffff;
175}
176
177.btn-info {
178  background: #00b4d8;
179  color: #ffffff;
180}
181
182.btn:hover {
183  transform: scale(1.05);
184  box-shadow: 0 5px 15px rgba(0, 255, 136, 0.4);
185}
186
187.status-section {
188  background: rgba(0, 180, 216, 0.1);
189  border: 2px solid #00b4d8;
190  border-radius: 10px;
191  padding: 1.5rem;
192  margin-bottom: 2rem;
193}
194
195.messages {
196  margin-top: 1rem;
197}
198
199.message {
200  padding: 1rem;
201  margin: 0.5rem 0;
202  border-radius: 5px;
203  border-left: 4px solid;
204}
205
206.message-success {
207  background: rgba(0, 255, 136, 0.2);
208  border-color: #00ff88;
209}
210
211.message-info {
212  background: rgba(0, 180, 216, 0.2);
213  border-color: #00b4d8;
214}
215
216.info-box {
217  background: rgba(255, 255, 255, 0.05);
218  border: 2px solid #00ff88;
219  border-radius: 10px;
220  padding: 1.5rem;
221}
222
223.info-box ul {
224  list-style: none;
225  padding: 0;
226}
227
228.info-box li {
229  padding: 0.5rem 0;
230  color: #ffffff;
231}
232
233.info-box strong {
234  color: #00b4d8;
235}
236</style>

Le second éditeur est un laboratoire de modificateurs : envoie son formulaire avec .prevent, clique sur les boutons avec .stop et .once, et essaie Entrée, Échap et le raccourci Ctrl+S dans les champs de texte.

Code de cette leçon : App.vue
1<script setup>
2import { ref } from 'vue'
3
4const commandInput = ref('')
5const searchQuery = ref('')
6const formData = ref({
7  mission: '',
8  priority: 'medium',
9  description: ''
10})
11
12const logs = ref([])
13const systemMessages = ref([
14  { id: 1, text: 'Système initialisé' },
15  { id: 2, text: 'Connexion établie avec la base martienne' },
16  { id: 3, text: 'Tous les systèmes opérationnels' }
17])
18let nextMessageId = 4
19
20function handleCommand(event) {
21  event.preventDefault()
22  if (commandInput.value.trim()) {
23    logs.value.unshift({
24      id: Date.now(),
25      command: commandInput.value,
26      timestamp: new Date().toLocaleTimeString()
27    })
28    commandInput.value = ''
29  }
30}
31
32function preventRefresh(event) {
33  event.preventDefault()
34  alert('Le formulaire a été arrêté par event.preventDefault()')
35}
36
37function stopPropagation(event) {
38  event.stopPropagation()
39  systemMessages.value.unshift({ id: nextMessageId++, text: 'Bouton cliqué - propagation arrêtée' })
40}
41
42function handleContainerClick() {
43  systemMessages.value.unshift({ id: nextMessageId++, text: 'Conteneur cliqué' })
44}
45
46function runOnce() {
47  systemMessages.value.unshift({ id: nextMessageId++, text: 'Ce gestionnaire ne s\'exécute qu\'une seule fois !' })
48}
49
50function handleEnter() {
51  systemMessages.value.unshift({ id: nextMessageId++, text: `Recherche exécutée : ${searchQuery.value}` })
52  searchQuery.value = ''
53}
54
55function handleEscape() {
56  searchQuery.value = ''
57  systemMessages.value.unshift({ id: nextMessageId++, text: 'Recherche effacée avec ÉCHAP' })
58}
59
60function handleCtrlS(event) {
61  event.preventDefault()
62  systemMessages.value.unshift({ id: nextMessageId++, text: 'Mission enregistrée (Ctrl+S)' })
63}
64
65function submitMission(event) {
66  event.preventDefault()
67  logs.value.unshift({
68    id: Date.now(),
69    command: `Mission : ${formData.value.mission} | Priorité : ${formData.value.priority}`,
70    timestamp: new Date().toLocaleTimeString()
71  })
72  formData.value.mission = ''
73  formData.value.description = ''
74}
75</script>
76
77<template>
78  <div class="modifiers-lab">
79    <h1>Laboratoire de modificateurs - NOVA LAB</h1>
80
81    <!-- Section des modificateurs d'événement -->
82    <section class="lab-section">
83      <h2>Modificateurs d'événement</h2>
84
85      <!-- Modificateur .prevent -->
86      <div class="demo-card">
87        <h3>.prevent - Empêcher l'action par défaut</h3>
88        <form @submit.prevent="preventRefresh" class="demo-form">
89          <input
90            type="text"
91            placeholder="Ce formulaire ne rechargera pas la page"
92            class="input-field"
93          >
94          <button type="submit" class="btn btn-primary">
95            Envoyer (preventDefault)
96          </button>
97        </form>
98        <code class="code-hint">@submit.prevent="preventRefresh"</code>
99      </div>
100
101      <!-- Modificateur .stop -->
102      <div class="demo-card">
103        <h3>.stop - Arrêter la propagation de l'événement</h3>
104        <div @click="handleContainerClick" class="click-container">
105          Clique sur le conteneur (l'événement se propage)
106          <button @click.stop="stopPropagation" class="btn btn-warning">
107            Clique sur le bouton (propagation arrêtée)
108          </button>
109        </div>
110        <code class="code-hint">@click.stop="stopPropagation"</code>
111      </div>
112
113      <!-- Modificateur .once -->
114      <div class="demo-card">
115        <h3>.once - Le gestionnaire ne s'exécute qu'une fois</h3>
116        <button @click.once="runOnce" class="btn btn-success">
117          Clique-moi (ne fonctionne qu'une fois)
118        </button>
119        <code class="code-hint">@click.once="runOnce"</code>
120      </div>
121
122      <!-- Modificateurs multiples -->
123      <div class="demo-card">
124        <h3>Combiner les modificateurs</h3>
125        <form @submit.prevent.stop="handleCommand" class="demo-form">
126          <input
127            v-model="commandInput"
128            type="text"
129            placeholder="Saisis une commande..."
130            class="input-field"
131          >
132          <button type="submit" class="btn btn-primary">
133            Exécuter la commande
134          </button>
135        </form>
136        <code class="code-hint">@submit.prevent.stop="handleCommand"</code>
137      </div>
138    </section>
139
140    <!-- Section des modificateurs de touche -->
141    <section class="lab-section">
142      <h2>Modificateurs de touche</h2>
143
144      <!-- Modificateur .enter -->
145      <div class="demo-card">
146        <h3>.enter - Détecte la touche Entrée</h3>
147        <input
148          v-model="searchQuery"
149          @keyup.enter="handleEnter"
150          type="text"
151          placeholder="Tape puis appuie sur Entrée..."
152          class="input-field"
153        >
154        <code class="code-hint">@keyup.enter="handleEnter"</code>
155      </div>
156
157      <!-- Modificateur .esc -->
158      <div class="demo-card">
159        <h3>.esc - Détecte la touche Échap</h3>
160        <input
161          v-model="searchQuery"
162          @keyup.esc="handleEscape"
163          type="text"
164          placeholder="Tape puis appuie sur ÉCHAP pour effacer..."
165          class="input-field"
166        >
167        <code class="code-hint">@keyup.esc="handleEscape"</code>
168      </div>
169
170      <!-- Touches modificatrices système -->
171      <div class="demo-card">
172        <h3>.ctrl + touche - Combinaisons de touches</h3>
173        <textarea
174          v-model="formData.description"
175          @keydown.ctrl.s="handleCtrlS"
176          placeholder="Appuie sur Ctrl+S pour enregistrer..."
177          class="textarea-field"
178          rows="4"
179        ></textarea>
180        <code class="code-hint">@keydown.ctrl.s="handleCtrlS"</code>
181      </div>
182
183      <!-- Modificateurs de touche multiples -->
184      <div class="demo-card">
185        <h3>Formulaire complet avec modificateurs</h3>
186        <form @submit.prevent="submitMission" class="mission-form">
187          <input
188            v-model="formData.mission"
189            type="text"
190            placeholder="Nom de la mission (Entrée pour envoyer)..."
191            class="input-field"
192          >
193          <select v-model="formData.priority" class="select-field">
194            <option value="low">Priorité basse</option>
195            <option value="medium">Priorité moyenne</option>
196            <option value="high">Priorité haute</option>
197          </select>
198          <button type="submit" class="btn btn-primary">
199            Envoyer la mission
200          </button>
201        </form>
202      </div>
203    </section>
204
205    <!-- Messages système -->
206    <section class="lab-section">
207      <h2>Messages système</h2>
208      <div class="messages-container">
209        <div
210          v-for="msg in systemMessages.slice(0, 10)"
211          :key="msg.id"
212          class="message-item"
213        >
214          <span class="message-time">{{ new Date().toLocaleTimeString() }}</span>
215          <span class="message-text">{{ msg.text }}</span>
216        </div>
217      </div>
218    </section>
219
220    <!-- Journaux de commandes -->
221    <section class="lab-section" v-if="logs.length > 0">
222      <h2>Journaux de commandes</h2>
223      <div class="logs-container">
224        <div
225          v-for="log in logs"
226          :key="log.id"
227          class="log-item"
228        >
229          <span class="log-time">{{ log.timestamp }}</span>
230          <span class="log-command">{{ log.command }}</span>
231        </div>
232      </div>
233    </section>
234
235    <!-- Encadré d'information -->
236    <div class="info-box">
237      <h3>Modificateurs d'événement et de touche dans Vue</h3>
238      <div class="info-grid">
239        <div>
240          <h4>Modificateurs d'événement :</h4>
241          <ul>
242            <li><strong>.prevent</strong> - event.preventDefault()</li>
243            <li><strong>.stop</strong> - event.stopPropagation()</li>
244            <li><strong>.once</strong> - gestionnaire une seule fois</li>
245            <li><strong>.capture</strong> - mode capture</li>
246            <li><strong>.self</strong> - seulement si target === currentTarget</li>
247            <li><strong>.passive</strong> - { passive: true }</li>
248          </ul>
249        </div>
250        <div>
251          <h4>Modificateurs de touche :</h4>
252          <ul>
253            <li><strong>.enter</strong> - touche Entrée</li>
254            <li><strong>.tab</strong> - touche Tab</li>
255            <li><strong>.delete</strong> - Suppr/Retour arrière</li>
256            <li><strong>.esc</strong> - touche Échap</li>
257            <li><strong>.space</strong> - touche Espace</li>
258            <li><strong>.up/.down/.left/.right</strong> - touches fléchées</li>
259            <li><strong>.ctrl/.alt/.shift/.meta</strong> - touches système</li>
260          </ul>
261        </div>
262      </div>
263    </div>
264  </div>
265</template>
266
267<style scoped>
268.modifiers-lab {
269  background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
270  min-height: 100vh;
271  padding: 2rem;
272  color: #ffffff;
273  font-family: 'Courier New', monospace;
274}
275
276h1 {
277  text-align: center;
278  color: #00ff88;
279  text-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
280  margin-bottom: 3rem;
281}
282
283h2 {
284  color: #00b4d8;
285  margin-bottom: 1.5rem;
286  border-bottom: 2px solid #00b4d8;
287  padding-bottom: 0.5rem;
288}
289
290h3 {
291  color: #00ff88;
292  font-size: 1.1rem;
293  margin-bottom: 1rem;
294}
295
296.lab-section {
297  background: rgba(0, 255, 136, 0.05);
298  border: 2px solid #00ff88;
299  border-radius: 10px;
300  padding: 2rem;
301  margin-bottom: 2rem;
302}
303
304.demo-card {
305  background: rgba(0, 180, 216, 0.1);
306  border: 2px solid #00b4d8;
307  border-radius: 8px;
308  padding: 1.5rem;
309  margin-bottom: 1.5rem;
310}
311
312.demo-form {
313  display: flex;
314  gap: 1rem;
315  margin-bottom: 1rem;
316  flex-wrap: wrap;
317}
318
319.mission-form {
320  display: flex;
321  flex-direction: column;
322  gap: 1rem;
323}
324
325.input-field, .select-field, .textarea-field {
326  background: rgba(0, 0, 0, 0.3);
327  border: 2px solid #00b4d8;
328  border-radius: 5px;
329  padding: 0.75rem;
330  color: #ffffff;
331  font-family: 'Courier New', monospace;
332  font-size: 1rem;
333  flex: 1;
334  min-width: 200px;
335}
336
337.input-field:focus, .select-field:focus, .textarea-field:focus {
338  outline: none;
339  border-color: #00ff88;
340  box-shadow: 0 0 10px rgba(0, 255, 136, 0.3);
341}
342
343.input-field::placeholder, .textarea-field::placeholder {
344  color: rgba(255, 255, 255, 0.5);
345}
346
347.btn {
348  padding: 0.75rem 1.5rem;
349  border: none;
350  border-radius: 5px;
351  font-size: 1rem;
352  font-weight: bold;
353  cursor: pointer;
354  transition: all 0.3s ease;
355  font-family: 'Courier New', monospace;
356}
357
358.btn-primary {
359  background: #00ff88;
360  color: #0a0e27;
361}
362
363.btn-warning {
364  background: #ffb400;
365  color: #0a0e27;
366}
367
368.btn-success {
369  background: #00b4d8;
370  color: #ffffff;
371}
372
373.btn:hover {
374  transform: scale(1.05);
375  box-shadow: 0 5px 15px rgba(0, 255, 136, 0.4);
376}
377
378.click-container {
379  background: rgba(255, 180, 0, 0.1);
380  border: 2px dashed #ffb400;
381  border-radius: 5px;
382  padding: 2rem;
383  text-align: center;
384  cursor: pointer;
385  transition: all 0.3s ease;
386  margin-bottom: 1rem;
387}
388
389.click-container:hover {
390  background: rgba(255, 180, 0, 0.2);
391}
392
393.code-hint {
394  display: block;
395  background: rgba(0, 0, 0, 0.5);
396  border-left: 4px solid #00ff88;
397  padding: 0.75rem;
398  border-radius: 5px;
399  color: #00ff88;
400  font-family: 'Courier New', monospace;
401  font-size: 0.9rem;
402}
403
404.messages-container, .logs-container {
405  background: rgba(0, 0, 0, 0.3);
406  border-radius: 8px;
407  padding: 1rem;
408  max-height: 300px;
409  overflow-y: auto;
410}
411
412.message-item, .log-item {
413  padding: 0.75rem;
414  background: rgba(0, 180, 216, 0.1);
415  border-left: 4px solid #00b4d8;
416  border-radius: 5px;
417  margin-bottom: 0.5rem;
418  display: flex;
419  gap: 1rem;
420  align-items: center;
421}
422
423.message-time, .log-time {
424  color: #ffb400;
425  font-size: 0.9rem;
426  min-width: 100px;
427}
428
429.message-text, .log-command {
430  color: #ffffff;
431  flex: 1;
432}
433
434.info-box {
435  background: rgba(0, 255, 136, 0.05);
436  border: 2px solid #00ff88;
437  border-radius: 10px;
438  padding: 2rem;
439}
440
441.info-box h3 {
442  margin-top: 0;
443  margin-bottom: 1.5rem;
444}
445
446.info-grid {
447  display: grid;
448  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
449  gap: 2rem;
450}
451
452.info-box h4 {
453  color: #00b4d8;
454  margin-bottom: 1rem;
455}
456
457.info-box ul {
458  list-style: none;
459  padding: 0;
460}
461
462.info-box li {
463  padding: 0.5rem 0;
464  color: #ffffff;
465  border-left: 3px solid #00b4d8;
466  padding-left: 1rem;
467  margin: 0.5rem 0;
468}
469
470.info-box strong {
471  color: #00ff88;
472}
473</style>

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. À quoi sert la directive v-bind ?

  2. 2. Quel est le raccourci de v-bind:href ?

  3. 3. À quoi sert v-on ?

  4. 4. Que signifie <img :src> sans valeur quand une variable src existe dans <script setup> ?

  5. 5. Que fait .prevent dans @submit.prevent ?

  6. 6. Que fait le modificateur .stop dans v-on ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Utilise v-bind (raccourci :) pour affecter dynamiquement les attributs src, alt, href et disabled.

  • Clique dans l'ordre

    Ordonne la syntaxe de gestion du clic avec un argument :

  • Éditeur de code

    Utilise v-on (ou son raccourci @) pour gérer les événements click, submit et keyup.

  • Tri horizontal

    Ordonne la syntaxe correcte de v-bind pour un attribut src dynamique :

  • Tri horizontal

    Ordonne la syntaxe correcte de v-on pour gérer un clic :

  • Tri vertical

    Ordonne les étapes de gestion d'un événement clic dans Vue :

  • Éditeur de code

    Utilise les modificateurs .prevent, .stop et .once dans la gestion des événements.