Tutoriel Angular – Améliorations : Compilation de Code, Affichage Markdown et Mode de Chargement

Par dbeulze, 21 mars, 2025

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 et style) 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.

drawing drawing drawing drawing

Alors bien sur il manque beaucoup de chose pour que cela soit comme Progression mais nous somme sur une bonne piste !

Références


Étiquettes

Commentaires