Dans ce nouveau blog, nous allons apporter plusieurs modifications à notre application Angular pour améliorer l'expérience utilisateur ainsi qu'ajouter de nouveau élément. Nous verrons comment :
- Modifier le composant
exercice-sortie
pour exécuter des tests et afficher la sortie formatée en Markdown. - Mettre à jour le service YAML pour ajouter deux attributs (
execution
etstyle
) aux tests. - Créer un nouveau service de compilation, qui s'appuie sur l'API Judge0.
- Modifier le composant
bouton
pour intégrer un mode de chargement. - Ajouter un observable dans le service
exercices
pour suivre le code et le langage. - Afficher correctement l'énoncé en Markdown dans le composant
exercice-enoncer
.
1. Mise à jour du composant exercice-sortie
HTML - exercice-sortie.component.html
<div class="d-flex w-100 flex-column h-100 mt-3 ">
<div class="d-flex flex-column align-items-center gap-2 justify-content-center text-center">
<h4>Tests:</h4>
<app-bouton [customStyle]="test.style" [loadingMode]="test.execution"
(click)="executerTest(test)" *ngFor="let test of tests"
[title]="test.nom" class="w-100">
</app-bouton>
</div>
<div class="h-100">
<h4>Sortie:</h4>
<div [innerHTML]="sortie"></div>
</div>
</div>
TypeScript - exercice-sortie.component.ts
import { Component, Input, OnInit } from '@angular/core';
import { Question } from '../../entities/question';
import { ExercicesService } from '../../services/exercices.service';
import { BoutonComponent } from '../bouton/bouton.component';
import { YamlService } from '../../services/yaml.service';
import { NgForOf } from '@angular/common';
import { CompilateurService } from '../../services/compilateur.service';
import { HttpClient } from '@angular/common/http';
import { MarkdownService } from 'ngx-markdown';
import { Observable } from 'rxjs';
@Component({
selector: 'app-exercice-sortie',
imports: [
BoutonComponent,
NgForOf
],
templateUrl: './exercice-sortie.component.html',
styleUrl: './exercice-sortie.component.css'
})
export class ExerciceSortieComponent implements OnInit {
@Input() exercice: Question = {
auteur: '',
licence: '',
niveau: '',
objectif: '',
rétroactions: { negative: '', positive: '' },
tests: '',
titre: '',
type: '',
ebauches: {},
énoncé: ''
};
code: string = "";
langageId: number = 0;
tests: any;
sortie: string | Promise<string> = "";
constructor(
private markdownService: MarkdownService,
private exerciceService: ExercicesService,
private yamlService: YamlService,
private compilateur: CompilateurService
) {}
ngOnInit() {
// Récupération de l'exercice courant et des tests YAML
this.exercice = this.exerciceService.exercice;
if (this.exercice.tests) {
this.yamlService.readYAML(this.exercice.tests.replace("https://git.dti.crosemont.quebec/", "/api/"))
.subscribe((yaml) => {
this.tests = this.yamlService.convertYAMLToTest(yaml);
});
}
// Souscription pour récupérer le code et le langage sélectionné
this.exerciceService.code$.subscribe(code => {
this.code = code;
this.exerciceService.langage$.subscribe(langage => {
this.compilateur.getLanguage(langage).subscribe(languageId => {
this.langageId = languageId;
});
});
});
}
executerTest(test: any) {
let output = "";
test.execution = true;
const data = {
source_code: this.code,
language_id: this.langageId,
};
this.compilateur.compiler(data).subscribe({
next: (value) => {
// On attend quelques secondes avant de récupérer le résultat
setTimeout(() => {
this.compilateur.getSubmission(value.token).subscribe({
next: (result) => {
if (result.status.description != 'Accepted') {
output = result.stderr;
output += result.message;
output += result.compile_output;
test.style = "background: rgb(207 57 57);color: white;width: 95%;";
} else {
test.style = "background: rgb(62 179 100);color: white;width: 95%;";
output = result.stdout;
}
test.execution = false;
},
error: (error) => {
console.error(error);
},
complete: () => {
this.sortie = this.markdownService.parse(output);
}
});
}, 5000);
},
error: (error) => {
console.error(error);
},
complete: () => {}
});
}
}
Explication :
- Le composant affiche un bouton pour chaque test. Lorsqu'un bouton est cliqué, la méthode
executerTest
compile et exécute le code via l'API Judge0. - Pendant l'exécution, la propriété
loadingMode
active un spinner sur le bouton. - La sortie de l'exécution est transformée en HTML grâce à
MarkdownService
pour un rendu formaté.
2. Mise à jour du service YAML
Nous modifions la fonction convertYAMLToTest
pour ajouter les attributs execution
et style
aux tests.
convertYAMLToTest(yaml: string): Tests[] {
let result: Tests[] = [];
try {
const parsedData: any = jsyaml.load(yaml);
const tests: Tests[] = parsedData.map((test: any) => {
const entreeStr = typeof test['entrée'] === 'string' ? test['entrée'] : String(test['entrée']);
const sortieStr = typeof test.sortie === 'string' ? test.sortie : String(test.sortie);
return {
nom: test.nom,
entree: entreeStr
.trim()
.split('\n')
.map(line => Number(line.trim()))
.filter(n => !isNaN(n)),
sortie: sortieStr
.trim()
.split('\n')
.map((line: any) => Number(line.trim()))
.filter((n: any) => !isNaN(n)),
execution: false,
style: "width: 95%;"
};
});
result = tests;
} catch (error) {
console.error("Erreur lors de la conversion du YAML :", error);
}
return result;
}
Explication :
- Chaque test se voit désormais attribuer une propriété
execution
pour indiquer son état d'exécution et une propriétéstyle
pour personnaliser l'apparence du bouton.
3. Nouveau service : CompilateurService
Ce service gère la compilation du code en appelant l'API Judge0.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class CompilateurService {
compilateurUrl: string = "http://10.0.0.235:2358";
constructor(private http: HttpClient) { }
compiler(data: any): Observable<any> {
console.log("Data envoyer: " + JSON.stringify(data));
return this.http.post(this.compilateurUrl + '/submissions/?base64_encoded=false&wait=false', data);
}
getLanguages(): Observable<any> {
return this.http.get(this.compilateurUrl + '/languages/');
}
getSubmission(token: string): Observable<any> {
return this.http.get(this.compilateurUrl + `/submissions/${token}?base64_encoded=false`);
}
getLanguage(name: string): Observable<number> {
return this.http.get<any[]>(this.compilateurUrl + '/languages/').pipe(
map(languages => {
const language = languages.find(lang =>
lang.name.toLowerCase().includes(name.toLowerCase())
);
return language ? language.id : 0;
})
);
}
}
Explication :
- Ce service utilise l'API Judge0 pour compiler le code et récupérer les résultats.
- La méthode
getLanguage
permet d'obtenir l'identifiant du langage en fonction d'un nom.
4. Modification du composant bouton
Le composant bouton
est mis à jour pour intégrer un mode de chargement.
HTML - bouton.component.html
<button [style]="customStyle" [style.color]="colorText" (click)="click()">
<div *ngIf="loadingMode" class="spinner-border" role="status"></div>
<ng-container *ngIf="!loadingMode">{{ title }}</ng-container>
</button>
TypeScript - bouton.component.ts
@Input() loadingMode: boolean = false;
Explication :
- Lorsque
loadingMode
est activé, un spinner est affiché pour indiquer que l'action est en cours.
5. Ajout d'un Observable dans le service exercices
Pour suivre le code et le langage sélectionné, nous avons ajouté un BehaviorSubject
dans le service exercices
.
import { BehaviorSubject } from 'rxjs';
langageSubject = new BehaviorSubject<string>("");
langage$ = this.langageSubject.asObservable();
codeSubject = new BehaviorSubject<string>("");
code$ = this.codeSubject.asObservable();
setCode(code: any) {
this.codeSubject.next(code);
}
setLangage(langage:any): void {
this.langageSubject.next(langage);
}
Explication :
- Cet observable permet aux différents composants de se synchroniser sur les mises à jour du code source et du langage.
6. Installation de la bibliothèque Markdown pour Angular
Pour afficher correctement l'énoncé et la sortie en Markdown, installez la librairie ngx-markdown :
npm install ngx-markdown --save
Explication :
- Cette bibliothèque transforme des chaînes au format Markdown en HTML, améliorant ainsi la lisibilité du contenu.
7. Modification du composant exercice-enoncer
Pour afficher l'énoncé en Markdown, remplacez la balise <p>
par une balise <div>
avec binding sur innerHTML
.
HTML - exercice-enoncer.component.html
<div [innerHTML]="enonce"></div>
TypeScript - exercice-enoncer.component.ts
import { MarkdownService } from 'ngx-markdown';
// ...
export class ExerciceEnoncerComponent implements OnInit {
enonce: string | Promise<string> = "";
constructor(
private router: Router,
private exerciceService: ExercicesService,
private markdownService: MarkdownService
) {}
ngOnInit() {
this.exercice = this.exerciceService.exercice;
this.enonce = this.markdownService.parse(this.exercice.énoncé);
}
// ...
}
Explication :
- L'énoncé est transformé en HTML via
MarkdownService
pour conserver la mise en forme Markdown initiale.
Votre mini Progression est désormais fonctionnel, ci-dessous vous trouverez une démo vidéo ainsi que des captures d'écran de l'application.




Alors bien sur il manque beaucoup de chose pour que cela soit comme Progression mais nous somme sur une bonne piste !
Références
- GOOGLE, Angular Team, dir. Angular, 2024, https://angular.io
- NPM, Inc., dir. ngx-monaco-editor‑v2, 2024, https://www.npmjs.com/package/ngx-monaco-editor-v2
- Judge0, GitHub, https://github.com/judge0/judge0
- Tutoriel Judge0 : How to self-host Judge0 API on your PC
- NPM, Inc., dir. ngx-markdown, NPM, 2024 https://www.npmjs.com/package/ngx-markdown
Commentaires