Tutoriel Angular – Ajout de nouveaux composants et mise à jour du service YAML

Par dbeulze, 14 mars, 2025

Dans cette nouvelle partie, nous allons enrichir notre application Angular en ajoutant deux nouveaux composants (Selecteur et ZoneText) et en modifiant le service YAML pour mieux gérer les tests. Nous verrons également comment ces modifications impactent les composants existants exercice-sortie et exercice-code.


1. Ajout des nouveaux composants

1.1 Composant Selecteur

Ce composant permet à l'utilisateur de choisir une option dans une liste déroulante. Il est notamment utilisé dans le composant exercice-code pour sélectionner le langage de programmation souhaité.

HTML - selecteur.component.html

<select [ngModel]="selectedOption" (ngModelChange)="onChange($event)">
  <option [value]="option.toLowerCase()" *ngFor="let option of options">{{ option }}</option>
</select>

CSS - selecteur.component.css

select {
  padding: 0.5vh 2vh;
  border: none;
  background: #121212;
  border-radius: 5px;
  color: white;
}

TypeScript - selecteur.component.ts

import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { NgForOf } from '@angular/common';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-selecteur',
  imports: [
    NgForOf,
    FormsModule
  ],
  templateUrl: './selecteur.component.html',
  styleUrl: './selecteur.component.css'
})
export class SelecteurComponent implements OnInit {
  @Input() options: string[] = [];
  @Input() nom: string = "";
  @Output() selecteur = new EventEmitter<string>();
  selectedOption: string = "";

  onChange(optionValue: string) {
    this.selecteur.emit(optionValue);
  }

  ngOnInit(): void {
    this.selectedOption = this.options[0];
  }
}

Explication :

  • HTML : Utilisation du binding avec ngModel et d'une boucle *ngFor pour générer les options de la liste.
  • CSS : J'ai realiser un style assez simple mais vous pouvez le modifier comme vous le souhaitez.
  • TypeScript : Le composant initialise la sélection avec la première option et émet la valeur sélectionnée pour que le parent puisse l'utiliser.

1.2 Composant ZoneText

Le composant ZoneText servira à afficher les sorties lorsque nous lancerons les tests. Sa fonctionnalité sera améliorée dans les prochaines versions du tutoriel.


2. Mise à jour du service YAML

Pour mieux gérer les données de nos exercices, nous avons modifié le service YAML. L'objectif est de pouvoir convertir les définitions YAML en objets exploitables par l'application, notamment pour gérer les tests.

2.1 Conversion d'un YAML en Question

Nous allons modifier la méthode convertYAMLToQuestion .

convertYAMLToQuestion(yaml: string, url: string): Question {
    const includeType = new jsyaml.Type('!include', {
      kind: 'scalar',
      resolve: (data) => typeof data === 'string',
      construct: (data) => {
        return url.replace("info.yml", "").replace("?ref_type=heads", "") + data;
      }
    });
    const CUSTOM_SCHEMA = jsyaml.DEFAULT_SCHEMA.extend([includeType]);
    const parsedData = jsyaml.load(yaml, { schema: CUSTOM_SCHEMA });
    return parsedData as Question;
}

2.2 Conversion d'un YAML en Tests

La méthode convertYAMLToTest transforme le contenu YAML en un tableau d'objets de type 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 => Number(line.trim()))
            .filter(n => !isNaN(n))
        };
      });
      result = tests;
    } catch (error) {
      console.error("Erreur lors de la conversion du YAML :", error);
    }

    return result;
}

2.3 Définition de l'entité Tests

export interface Tests {
  nom: string;
  entree: number[];
  sortie: number[];
}

Explication :

  • Le service YAML est enrichi pour pouvoir traiter les tests et les intégrer dans nos exercices.
  • La conversion transforme les chaînes de caractères en tableaux de nombres pour faciliter les comparaisons lors des tests.

3. Modifications des composants existants

3.1 Mise à jour du composant Exercice-Sortie

Nous avons adapté le composant exercice-sortie pour qu'il affiche les boutons correspondant à chaque test et intègre le composant ZoneText pour afficher du contenu supplémentaire.

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">
      <app-bouton *ngFor="let test of tests" [title]="test.nom" class="w-100" customStyle="width: 95%;"></app-bouton>
  </div>
  <div>
    <app-zone-text></app-zone-text>
  </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 { ZoneTextComponent } from '../zone-text/zone-text.component';
import { YamlService } from '../../services/yaml.service';
import { NgForOf } from '@angular/common';

@Component({
  selector: 'app-exercice-sortie',
  imports: [
    BoutonComponent,
    ZoneTextComponent,
    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é: ''
  };

  tests: any;

  constructor(private exerciceService: ExercicesService, private yamlService: YamlService) {}

  ngOnInit() {
    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);
        });
    }
  }
}

Explication :

  • Le composant récupère l'exercice courant et, si des tests sont définis, il lit le fichier YAML correspondant pour génére les boutons de test.

3.2 Mise à jour du composant Exercice-Code

Le composant exercice-code a été modifié pour intégrer le Selecteur. Celui-ci permet à l'utilisateur de choisir le langage de programmation dans lequel le code sera affiché grâce à l'éditeur Monaco.

HTML - exercice-code.component.html

<div style="z-index: 9999;" class="position-fixed top-0 end-0 m-2 ">
  <app-selecteur (selecteur)="setLangage($event)" [options]="langages"></app-selecteur>
</div>
<ngx-monaco-editor class="code-editor" [options]="editorOptions" [(ngModel)]="code"></ngx-monaco-editor>

TypeScript - exercice-code.component.ts

import { Component, Input, OnInit } from '@angular/core';
import { Question } from '../../entities/question';
import { ExercicesService } from '../../services/exercices.service';
import { EditorComponent } from 'ngx-monaco-editor-v2';
import { FormsModule } from '@angular/forms';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { SelecteurComponent } from '../selecteur/selecteur.component';

@Component({
  selector: 'app-exercice-code',
  imports: [
    EditorComponent,
    FormsModule,
    SelecteurComponent
  ],
  templateUrl: './exercice-code.component.html',
  styleUrl: './exercice-code.component.css'
})
export class ExerciceCodeComponent implements OnInit {

  editorOptions = { theme: 'vs-light', language: 'javascript' };
  code: string = '';
  langages: string[] = [];
  selectedLanguage: string = 'java';

  @Input() exercice: Question = {
    auteur: '',
    licence: '',
    niveau: '',
    objectif: '',
    rétroactions: { negative: '', positive: '' },
    tests: '',
    titre: '',
    type: '',
    ebauches: {},
    énoncé: ''
  };

  constructor(private exerciceService: ExercicesService, private http: HttpClient) {}

  ngOnInit() {
    this.exercice = this.exerciceService.exercice;
    this.langages = Object.keys(this.exercice.ebauches);
    this.loadExerciceCode(this.langages[0]);
  }

  loadExerciceCode(langage: string) {
    this.readCode(this.exercice.ebauches[langage].replace("https://git.dti.crosemont.quebec/", "/api/"))
      .subscribe({
        next: (value) => {
          this.code = value;
        }
      });
  }

  readCode(url: string): Observable<string> {
    return this.http.get(url, { responseType: 'text' });
  }

  setLangage(value: string) {
    this.selectedLanguage = value;
    this.loadExerciceCode(this.selectedLanguage);
  }
}

CSS - exercice-code.component.css

.code-editor {
  height: 100%;
  width: 100%;
}

Explication :

  • Le Selecteur intégré permet de choisir dynamiquement le langage de programmation en fonction des clés présentes dans l'objet ebauches de l'exercice.
  • Une fois le langage sélectionné, le code correspondant est chargé et affiché dans l'éditeur Monaco.

Nous avons bientôt terminer cette partie, dans la prochaine veille techno nous allons nous approcher de la fin en y intégrant la compilation de code.


Références :


Commentaires