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-sortiepour exécuter des tests et afficher la sortie formatée en Markdown. - Mettre à jour le service YAML pour ajouter deux attributs (
executionetstyle) aux tests. - Créer un nouveau service de compilation, qui s'appuie sur l'API Judge0.
- Modifier le composant
boutonpour intégrer un mode de chargement. - Ajouter un observable dans le service
exercicespour 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
executerTestcompile et exécute le code via l'API Judge0. - Pendant l'exécution, la propriété
loadingModeactive un spinner sur le bouton. - La sortie de l'exécution est transformée en HTML grâce à
MarkdownServicepour 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é
executionpour indiquer son état d'exécution et une propriétéstylepour 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
getLanguagepermet 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
loadingModeest 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
MarkdownServicepour 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