Et si on revenait un peu à TARS

Mélange des origines avec la map de test

Petit point de situation suite à la mise à jour Angular 17. J’ai donc repris le POC TARS et sa carte de départ, j’ai nettoyé quelques POCs (CodingPark et le début du système de particules) et … ça ne marche plus :/ ?

Plus d’une fois j’ai eu le malheur d’un craquage complet avec un beau message d’erreur… et là je valide le fait que le message d’erreur est effectivement amélioré. Alors merci chère mémoire de ne pas m’aider à me rappeler pourquoi j’ai coder ça ainsi et merci les 4 ans passés à évoluer et donc ne plus écrire ou penser de la même manière. Ceci dit j’étais quand même dans le pétrin.

Je dirais principalement que le fait de ne plus compter sur les effets de bords du typage aide pas mal à avoir des soucis, du coup merci le mode stricte ^^. Puis surtout, en terme de documentation c’est tellement plus confortable.

C’est là aussi que l’on voit que l’on code en « happy path » et sans gestion rigoureuse d’erreur sur projet perso. Le « je sais ce que je fais car je l’ai fait » c’est bien, mais 4 ans plus tard… comment dire, ça laisse à désirer. Du coup « oui », TARS a des lacunes. J’ai tenté de dire que le sol était un sol et il n’aimait pas, imaginez ma surprise… et il fallait lui dire que c’était une « image »… logique ! Et pourquoi ? Du fait que la description de l’élément ne répondait pas au standard ISO(métrique) demandé, mais sans gestion d’erreur ça explose ailleurs et tu peux chercher longtemps pourquoi.

Comme le fait de cibler un élément d’un tableau sur son index, de modifier le tableau et d’avoir une erreur d’accès à une méthode sur une instance… tellement évident… heureusement ça c’est le côté POC pour générer la map et tester le moteur, l’idée étant de passer par l’éditeur ensuite qui lui ne fera pas d’erreur. Mais son opérateur, ça…

Ah et en passant, tant qu’à faire de l’amélioration générale, n’oubliez pas que A || B n’est pas pareil que A ?? B (petit lien sympa). Et que les ternaires A ? A : B peuvent devenir A ?? B.

Mais aussi que les pipes RxJs peuvent s’agrémenter de code perso et ainsi pouvoir charger un Object { a: 1, b: 2 } vers un tableau [{ name: a, value: 1 }, {...}].

Ou encore qu’on ne peut pas retourner la déclaration d’un enum pour accéder à ses clefs (return SO0), mais qu’il faudra retourner ses clefs directement (return Object.values(SO0)).

Enfin voilà, en gros, tout est une bonne grosse question de type et de ce que l’on peut faire avec. Il y aurait encore à en dire, mais au final je n’ai pas retenu ces directions, et sans les notes il sera dur de vous en montrer plus.

Il y a bien sûr encore pas mal de boulot, je vais repasser progressivement, à temps perdu, dans l’ensemble du POC et de TARS lui-même, faire un coup de clean et de typage massif, gestion d’erreur(s), et surement de refactorisation plus ou moins importante, par exemple la suppression du mode 2D, ce qui aiderait quelques petite performances; mais aussi le fait rencontré que le validateur de chemin n’ait pas l’information disponible de la différence de hauteur/niveau/élévation entre 2 Coords en comparaison, et qu’il sera difficile en l’état d’y arriver. Du coup, on reparle de théorie du système, doit-il gérer Z ou pas, gros débat (ombre portée dynamique, lumière, changement de niveau (hauteur), …).

Quand on pense qu’au départ de ce moment je voulais juste reprendre le dev de l’éditeur…

Y replonger, bien que difficile, chronophage et casse-gueule avec le temps et la mentalité différente aujourd’hui, est amusant, un petit challenge sur le côté, et surtout : il fonctionne, pour ce qu’il est, ses limites et possibilités.

Je me tâte toujours, est-ce que Nahyan peut être exploré en isométrique ou doit-on absolument le voir en 3D ?

Angular : 4 ans, 9 versions

Début 2019 on a Angular 8, et en cette fin 2023 on a reçus la 17, soit 9 versions ! Et pourquoi je vous dis ça ? Tout simplement car j’ai voulu mettre à jour le POC de TARS, et ça ne s’est pas du tout passé comme prévu malgré l’usage du site de migration qui est bien fait.

Le truc c’est que ce n’est pas exempt de soucis qui se pose au fil du temps. Par exemple, certaines commandes vont exécuter l’install (npm i), mais il vous faudra reprendre à la main en précisant le flag --legacy-peer-devs ou encore des résolutions d’arbre de dépendances qui ne se font plus correctement.

J’ai réussi non sans mal à passer à la version 11 mais pas plus loin, la quantité de conflits ou de soucis a explosé, pourtant le projet n’a rien de particulier d’un point de vue Angular.

Du coup j’ai opté pour une autre approche. Le projet n’utilise Angular que comme une coquille de lancement avec l’usage de l’injecteur et du client HTTP. J’ai donc créé un projet frais et j’ai replacé le code en dedans et là : BOOM ! Fallait pas oublier qu’on était passé en mode stricte, ou que le côté standalone a changé la manière de monter un module ou de gérer les composants, bref il ne s’en sortait plus.

On sort l’huile de coude, on repasse partout, on s’arrache les cheveux sur le typage fort qui me manquait à l’époque et on solutionne avec quelques generic. On ajuste la nouvelle mouture standalone avec les imports et providers locaux et ça tourne ! Enfin, oui, mais ça nécessitera quelques heures avant que le builder arrête de péter une durite. À savoir que si vous avez un couac il peut perdre la compréhension de tout les tags Angular et votre erreur c’est 1 détails quelques part… Bonne chance.

J’en profite pour vous filer un tuyaux : penser à définir vos composant en standalone, rajouter ce qu’il vous faut localement comme CommonModule ou HttpClientModule.

Un truc qui continue de m’agacer, c’est ce double état undefined et null, car si vous spécifiez un attribut de classe comme optionnel vous utiliserez un ? à sa fin, mais si vous l’assignez suite à un find d’un tableau par exemple, ce qui peut vous rendre votre type ou null, vous aurez un conflit possible de type, sauf si on parle de test(s) avant assignation ou autre écriture à rallonge; ça manque quand même de cohérence.

Enfin soit, c’était amusant d’y retoucher, avec l’objectif de reprendre son éditeur en main, toujours pour ce projet éternel qu’est Nahyan.

Angular 17 recèle d’un tas de nouveautés qui, je pense, pourraient, avoir leur rôle à jouer, voire en refactorisation, mais je m’avance un peu. Par exemple Signal, la sortie de Zone, la nouvelle syntaxe de templating, etc.

PrimeNg vs AG Grid : rendu et composant de cellule

Je sors d’une mission dans laquelle j’ai été amené à travailler avec PrimeNg, qui propose dans un même framework tout ce dont on a besoin. Évidemment c’est joli sur papier mais dans la réalité ce n’est pas le cas, PrimeNg souffre d’un manque de flexibilité, mais ils ont tous ce soucis, donc bon, on fait avec.

Mise en place

Ici je vais vous parler du cas des tableaux, plus précisément de la partie rendu et composant de cellule, ce que AG Grid propose mais pas PrimeNg. Donc à nous d’ajouter une couche. On va créer un projet de POC avec ce dont on aura besoin (vous avez node lts, npm et un angular/cli) :

ng new primeng-simple-table
cd primeng-simple-table
npm i primeng-lts primeicons --save
npm i primeflex --save
npm i @angular/cdk@12 --save

Dans notre app.module.ts on va ajouter l’import du TableModule

import { TableModule } from 'primeng-lts/table'

...

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    TableModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

On va vider app.component.html, puis créer un composant qu’on va baptiser <simple-table> et dedans on va y mettre le code de base du tableau PrimeNg, dans sa version dynamique.

<p-table [columns]="cols" [value]="data" responsiveLayout="scroll">
  <ng-template pTemplate="header" let-columns>
      <tr>
          <th *ngFor="let col of columns">
              {{ col.header }}
          </th>
      </tr>
  </ng-template>
  <ng-template pTemplate="body" let-rowData let-columns="columns">
      <tr>
          <td *ngFor="let col of columns">
              {{ rowData[col.field] }}
          </td>
      </tr>
  </ng-template>
</p-table>

Notez les modifications de nom de variables et notre fichier .ts .

import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'simple-table',
  templateUrl: './simple-table.component.html'
})
export class SimpleTableComponent implements OnInit {
  @Input() data: any[] = []
  @Input() cols: any[] = []

  constructor() { }

  ngOnInit(): void {
  }

}

Et on l’appel dans notre app.component.html avec une déclaration de variable data et cols comme 2 tableaux vides pour le moment.

<simple-table [data]="data" [cols]="cols">
</simple-table>

On n’oubliera pas de définir la partie CSS sinon on aura rien de bon à l’écran :), notre style.css sera donc :

@import '~primeicons/primeicons.css';
@import '~primeng-lts/resources/themes/saga-blue/theme.css';
@import '~primeng-lts/resources/primeng.min.css';
@import '~primeflex/primeflex'

On compile, on regarde le résultat… vous n’avez pas grand chose !

Des colonnes dynamiques

On faisant un composant dynamique on perd la faculté de définir les colonnes manuellement, c’est le but, on veut un système générique qui le fasse pour nous suivant ce qu’on lui donne, on va lui donner le moyen de créer les colonnes désirées, et on va en profiter pour se créer un jeu de données pour les tests.

export interface ColumnDef {
  field: string
  header: string
}

On se crée donc une interface de définition de colonne et on passera un tableau de ColumnDef à notre composant pour définir ce que nous voulons. Dans ce premier jet nous avons l’attribut de donnée à utiliser pour la cellule (field) et le titre de colonne (header).

Jeu de données

Avant de mettre en pratique et jouer avec un exemple nous allons nous créer un jeu de données, prenons des vaisseaux spatiaux, au grand hasard. Voici la structure utilisée, déposé dans un fichier annexe qu’on importera :

export enum SpacecraftStatus {
  Active,
  Destroyed
}

export const spacecrafts: any[] = [
  { id: 1, name: 'Eagle 5', type: 1, status: SpacecraftStatus.Active, univers: 'Spaceballs' },
  ...
]

export const spacecraftTypes: any[] = [
  { id: 1, label: 'Transport' },

  ...
]

Définition des colonnes

Sur base de notre jeu de données et de l’interface ColumnDef nous allons définir nos colonnes pour notre tableau. J’en profite pour typer les variables cols en fonction. Dans app.component.ts nous modifierons tel que :

  data = spacecrafts

  cols: ColumnDef[] = [
    { field: 'id', header: 'ID' },
    { field: 'name', header: 'Name' },
    { field: 'type', header: 'Type' },
    { field: 'status', header: 'Status' },
    { field: 'univers', header: 'Univers' }
  ]

Magnifique, jusque là on a pas fait grand chose, PrimeNg nous donne la solution et nous avons juste typé plus proprement l’attribut column qu’ils proposent.

Rendu de cellule

Maintenant que nous avons une démo qui fonctionne avec des colonnes dynamiques, voyons ce qu’est le rendu de cellule qui nous manque. Prenez un premier cas avec la valeur status de notre jeu de données, nous voulons afficher un texte compréhensible, et non la valeur de l’enum, en fonction de l’état du vaisseau.

De base rien ne nous le permet, regardez la partie {{ rowData[col.field] }}, ceci ne fait qu’imprimer la valeur de l’attribut courant. Il faut se mettre entre la valeur et son rendu. Une solution : une méthode intermédiaire, optionnelle, et variable s’il vous plaît. Pour ce faire nous allons ajouter un attribut à notre interface ColumnDef.

export interface ColumnDef {
  field: string
  header: string
  renderer?: (rowData: any, field: string) => string
}

Désormais on peut lui donner une fonction qui recevra en paramètre l’entièreté des données de la ligne courante ainsi que le champs courant à rendre. Modifions notre définition pour cette colonne :

...
{
  field: 'status',
  header: 'Status',
  renderer: (rowData, field) => rowData[field] === SpacecraftStatus.Active ? 'Active' : 'Destroyed'
},
...

Jusque là ça ne change rien, il nous faut l’appliquer. Pour ce faire il y a 2 chose : que faire de ceux qui n’ont pas de méthode définie, et comment appliquer cette méthode. Pour la première partie nous allons définir une méthode par défaut sous forme de service et l’appliquer à la place de l’utilisateur à l’initialisation du composant. Pour la seconde nous utiliserons un container et la propriété [innerHTML].

Le service

Ce service contiendrait les solutions fournies de base, rien ne sert de réinventer la roue à chaque projet :).

import { Injectable } from "@angular/core"

@Injectable({
  providedIn: 'root'
})
export class CellRenderer {
  static none(rowData: any, field: string): string {
    const value: any = rowData ? rowData[field] : false

    if (value || value === 0) {
      return value
    }

    return ''
  }
}

Modification de l’@Input cols

On transforme notre variable en privée avec setter/getter pour avoir la main dessus en cas de modifications et pouvoir ainsi mettre les valeurs par défaut. On aide ainsi l’utilisateur de notre service à ne pas se casser la tête avec toutes nos options :).

  _cols: ColumnDef[] = []
  @Input() set cols(columns: any[]) {
    this._cols = columns
    this._cols.forEach((c) => {
      if (!c.renderer) {
        c.renderer = CellRenderer.none
      }
    })
  }
  get cols(): ColumnDef[] {
    return this._cols
  }

Mise en application

Il ne nous reste qu’à modifier notre rendu de valeur au sein de la cellule.

<td *ngFor="let col of columns">
  <div [outerHTML]="col.renderer(rowData, col.field)"></div>
</td>

Et nous voilà avec un rendu de cellule dynamique. Ceci n’est qu’un exemple, ce qui m’est arrivé le plus souvent étant une transformation de valeur booléenne ou de date partant d’un format variable (2022-03-15 ou un timestamp) pour aller vers une représentation francophone (15/03/2022). Dans le cas de la date, la fonction du CellRenderer appel un service spécifique central, qui pourra être appellé par un pipe ou autre service.

Composant de cellule

Au lieu d’une modification de texte, nous souhaitons parfois avoir un comportement, une interaction, avec la donnée ou la ligne, tel qu’un bouton, une liste ou autre type de composant. Ceux-ci ont des paramètres et des événements et il faut arriver à les glisser dynamiquement dans une cellule.

Nous allons jouer avec la colonne type en modifiant data pour l’exemple et voir comment un rendu de cellule aurait pu nous aider (ce n’est pas la seule solution).

  data = spacecrafts.map((m) => ({
    ...m,
    type: spacecraftTypes.find((f) => f.id == m.type)
  }))
{
  field: 'type',
  header: 'Type',
  renderer: (rowData, field) => rowData[field]?.label
},

Mais ici on va directement attaquer un gros morceau en transformant cette cellule en liste déroulante pour modifier le type du vaisseau, soyons fou. Pour ce faire il va nous falloir plusieurs morceaux :

  • Définir ce qu’est un composant par des interfaces,
  • Modifier l’interface ColumnDef pour ajouter une définition pour composant,
  • Créer le composant liste
  • Décrire une mécanique permettant d’insérer un composant au sein de la cellule,
  • Modifier le rendu de la cellule pour détecter s’il s’agit d’un composant ou d’un renderer.
  • Modifier la définition des colonnes pour intégrer les changements

Création des interfaces

De quoi avons-nous besoin ? De dire quel composant il s’agit, de pouvoir lui passer des options et de lui donner le moyen de nous revenir par des événements.

export interface CellComponent {
  component: any
  options?: any
  events?: any
}

Jusque là c’est assez générique, on lui dit qu’on pourra faire quelque chose, fort optionnel, et on spécifiera avec notre cas d’usage : une liste.

export interface CellComponentOptionsList {
  items: any[]
  itemValue?: string
  itemLabel?: string
}

export interface CellComponentEventsList {
  change: (originalEvent: any, rowData: any, field: any) =&gt; void
}

On aura besoin de lui donner la liste des options de la liste déroulante, et quel attribut utiliser comme valeur et comme texte à afficher. En retour nous serons alerté en cas de changement en nous retournant l’événement original du composant, ainsi que les données de la ligne et la colonne courante, ça peut toujours servir de donner les infos que l’on a.

Modification de l’interface ColumnDef

Comme nous avons nos types nous pouvons maintenant modifier notre interface de départ pour ajouter l’option composant.

export interface ColumnDef {
  field: string
  header: string
  renderer?: (rowData: any, field: string) => string
  components?: CellComponent[]
}

Au pluriel car une colonne pourrait contenir plus d’un bouton par exemple, laissons de la flexibilité.

Créer le composant liste

Dans un répertoire /components/cell-components j’ai créé le composant list. Nous nous baserons sur le composant Dropdown de PrimeNg, avec leur exemple custom content, ce n’est pas obligé, mais si vous allez dans le détail ça sera plus facile d’avoir déjà la main sur le rendu via les templates. Du coup en avant dans l’HTML. N’oublier pas d’ajouter DropdownModule et ses dépendances.

@NgModule({
  declarations: [
...
  ],
  imports: [
    BrowserModule,
    <strong>BrowserAnimationsModule</strong>,
    <strong>FormsModule</strong>,

...
    <strong>DropdownModule</strong>
  ],
<p-dropdown [options]="items" [(ngModel)]="value" [optionLabel]="itemLabel" [optionValue]="itemValue"
            [showClear]="true">
  <ng-template let-item pTemplate="selectedItem">
    {{ item[itemLabel] }}
  </ng-template>
  <ng-template let-item pTemplate="item">
    {{ item[itemLabel] }}
  </ng-template>
</p-dropdown>

Sur base de l’exemple qui parle de country, j’ai mis les nommer les variables que l’on va définir et nettoyer l’exemple, les détails comptent. La suite se passe dans le fichier .ts pour définir tout ce beau monde.

@Component({
  selector: 'cc-list',
  templateUrl: './list.component.html'
})
export class ListComponent implements OnInit {
  // Must be in a parent class
  options?: CellComponentOptionsList // Specific to list, any for generic
  events?: CellComponentEventsList
  rowData!: any
  field!: string

  // Specific to list
  items: any[] = []
  itemValue!: string
  itemLabel!: string

  value?: string

  ngOnInit(): void {
  }
}

Alors d’abord, si on avait plusieurs composant de cellule on aurait forcément une classe parente avec la partie commune, pour ce POC j’ai regroupé et annoté. Ensuite, vu qu’on viendra d’une cellule il sera normal et attendu que l’on passe ce que la cellule a à nous donner, à savoir rowData et field, ainsi que la définition spécifique à notre composant (options et events), venant de la config dans ColumnDef. Enfin, comme définit dans nos interfaces, les particularités spécifiques à la liste (items, itemValue et itemLabel). Notez encore la propriété value qui contiendra le choix courant et qui sera utilisé dans l’événement change.

Tel quel, le composant ne fera qu’exister avec une liste vide et les paramètres non-défini. Nous les ajouterons après pour que vous compreniez d’abord bien le chainage des attributs et les affectations. Restons simple, ça se complexifiera vite assez.

La mécanique d’insertion du composant dans la cellule

C’est là tout le truc, en un point du code HTML on doit dire à Angular : « je veux mon composant là et que tu le raccordes à mes options, événements, … ». Pour ce faire on va passer par une directive qui jouera le rôle d’hôte.

@Directive({
  selector: '[cellComponentsHost]'
})
export class CellComponentsHostDirective implements OnInit {
  private items!: CellComponent[]
  @Input('cellComponentsHost') set config(items: CellComponent[]) {
    this.items = items
    this.initContent()
  }

  @Input() rowData: any[] = []
  @Input() field = ''

  constructor(
    private vcRef: ViewContainerRef,
    private cfr: ComponentFactoryResolver
  ) { }

  ngOnInit(): void {
    this.initContent()
  }

  initContent(): void {
    // Clean
    this.vcRef.clear()

    // Add each item
    this.items.forEach((item) => {
      const factory = this.cfr.resolveComponentFactory<ListComponent>(item.component)

      const component = this.vcRef.createComponent<ListComponent>(factory)
      component.instance.rowData = this.rowData
      component.instance.field = this.field
      component.instance.options = item.options
      component.instance.events = item.events
    })
  }
}

Pour faire simple, cette directive récupère ce qu’on lui passe sous forme d’attributs : cellComponentsHost, rowData et field. Attention au casting ListComponent, là c’est la classe parent que l’on indique normalement. Ensuite elle crée le composant et le place dans le container sur lequel est placé la directive. N’oubliez pas de la déclarer.

@NgModule({
  declarations: [
...
    <strong>CellComponentsHostDirective</strong>
  ],

Modifier le rendu de la cellule

<td *ngFor="let col of columns">
  <ng-container *ngIf="!col.components"><div [outerHTML]="col.renderer(rowData, col.field)"></div></ng-container>
  <ng-container *ngIf="col.components" [cellComponentsHost]="col.components" [rowData]="rowData" [field]="col.field"></ng-container>
</td>

Pas de chipotage, on encapsule notre rendu de cellule précédent dans un container conditionnel, dans l’autre on y retrouve notre directive et les attributs qu’on lui passe.

Modification de la définition de colonnes

{
  field: 'type',
  header: 'Type',
  components: [
    {
      component: ListComponent,
      options: {
        items: spacecraftTypes,
        itemLabel: 'label',
        itemValue: 'id'
      } as CellComponentOptionsList,
      events: {
        change: (originalEvent: any, rowData: any, field: any) => {
          console.log(field + ' : ' + rowData[field])
        }
      } as CellComponentEventsList
    }
  ]
},

On lui indique donc le composant voulu et nos options, jusque là vous devriez pouvoir faire les liens :), et on sort en console pour vérifier que l’évent a donné quelque chose.

Compléter le composant liste

Nous avons une interface de définition et nous avons définit notre cas d’utilisation sur la colonne type. Ensuite, nous avons créé le composant sans l’implémenter, ainsi que la directive hôte. Enfin, nous avons mis en place l’usage de la directive. On pourrait tracer le cheminement ainsi :

  • Le tableau boucle sur les colonnes pour créer une ligne.
  • Il arrive sur une cellule et se pose la question : est-ce un composant ou non ?
    • Si non : il tente le rendu de cellule (none par défaut)
    • Si oui :
      • La directive est fournie des informations et se lance pour créer chaque composants demandés en son seing
      • Le composant nouvellement créé est prêt

Il est prêt ? Qu’est-ce que cela veut dire ? On a parlé de compléter le composant pourtant :/ … et en effet on pourrait s’arrêter là et utiliser options directement, excepter le onChange manquant, mais nous désirons peut-être appliquer des contrôles ou des détections préliminaires, et dans cette idée je vais vous montrer une possibilité.

  ngOnInit(): void {
    this.items = this.options?.items || []
    this.itemValue = this.options?.itemValue || 'id'
    this.itemLabel = this.options?.itemLabel || 'label'

    this.value = this.rowData[this.field]
  }

  change(event: any): void {
    if (event.stopPropagation) {
      event.stopPropagation()
    }

    this.rowData[this.field] = this.value

    this.events?.change(event, this.rowData, this.field)
  }

Ainsi nous avons tout qui est relié, prêt à tester, ce qui va nous donner ce résultat :

La dropdown n’est pas des plus esthétique, pour ça il suffit d’un peu de CSS, d’abord ajouter une classe puis la définir :

<p-dropdown [options]="items" [(ngModel)]="value" [optionLabel]="itemLabel" [optionValue]="itemValue"
            [showClear]="true" (onChange)="change($event)" class="full-width">
p-dropdown.full-width .p-dropdown {
  width: 100%;
}

Aller plus loin

Il est beau, il est joli, mais est-ce tout ? Seule l’imagination étant notre limite, la réponse évident est : non ! Tout dépend de votre besoin, de vos envies de flexibilités, de ce que vous souhaitez proposer. Pour ma part voici des pistes abordées et développées :

Reporter les options fournies par p-table

Ce n’est pas une évidence pour tout le monde, mais en dehors des choix fait pour votre composant (imposer le scroll, le système de colonnes, …) vous bloquez l’accès aux autres options proposées par p-table, du coup ceux qui veulent utiliser votre composant vont être bloqués/frustrés. Une bonne pratique est de faire des ponts pour reporter ces options autant que possible, merci le code source de la lib pour vous aider.

Ligne de filtres

PrimeNg permet d’ajouter des filtres mais en dynamique il vous faudra user d’une autre directive hôte et d’une interface, sans oublie l’@Input pour passer votre demande au composant.

Sélection

PrimeNg permet également de mettre en place un système de sélection de ligne(s), mais vu notre côté dynamique c’est à nous de réintégrer cette solution, de manière optionnelle. Ceci sous-entend d’ajouter des colonnes de sélection et de donner à p-table la configuration (none, single, multiple)

Classes CSS

On l’oublie souvent, mais le design ! Du coup vous voulez préciser pour telle colonne un style (classe CSS), il faudra faire évoluer la définition de colonnes et modifier l’HTML de notre composant pour l’intégrer dans la partie template, tant sur l’en-tête que sur la cellule. On peut également imaginer que l’en-tête et la cellule peuvent avoir des styles différents (couleur, alignement, …).

Le composant liste

Notre composant liste peut également être poussé plus loin, en activant le filtre et/ou en améliorant le rendu pour afficher un contenu plus complexe comme un préfix à la valeur, ce qui m’est souvent arrivé : [PRE – valeur affichée] .

Git

Je vous ai fait un Git avec ce POC 🙂

Kraken en boîte, ou comment tout réunir dans un seul composant

Suite de l’article Unwrap the kraken, ou comment faire du multicolonne avec des sous composants, où nous allons pousser le bouchon un cran plus loin.

Dans le dernier article nous avions notre composant qui s’occupait de s’extraire et une directive qui s’occupait de le remettre dans une boite, en gros. Mais bon, si le composant pouvait éviter de s’extraire pour être remis en boite ça serait quand même mieux.

Du coup l’idée est de lui rendre la notion « es-tu une ligne ? » via un @Input() row: boolean = true; En considérant qu’il l’est par défaut (au choix). Ensuite quand on crée le composant on agit selon la valeur de row.

À l’initialisation, si nous somme une ligne, nous allons transformer notre template enfant #tref en contenu de notre propre bloc via le ViewContainerRef pour le créer et le Renderer2 qui va l’injecter dans le nœud de notre composant courant. Ensuite on peut signaler à notre bloc courant, le composant en lui-même, que nous aimerions le décorer de quelques classes CSS pour en faire une row css au sens Bootstrap.

if (this.row) {
  // Transform template into inside elements
  Promise.resolve(null).then(() => {
    const view = this.vcRef.createEmbeddedView(this.tpl);
    view.rootNodes.formEach((node) => this.renderer.appendChild(this.el.nativeElement, node));
  });

  // Transform parent to row with classes
  this.renderer.setAttribute(this.el.nativeElement, 'class', "row " + this.classes);
}

Quand l’élément est prêt, et que nous ne sommes pas une ligne, on continue de sortir notre contenu en dehors du bloc par la même mécanique. On ajoute une classe css au bloc courant pour le masquer (bootstrap: d-none -> display: none) et par la même mécanique que le paragraphe précédent on utilise le template et l’ajoute un cran plus haut du coup. Aussi simple que ça.

if (!this.row) {
  // Hide parent
  this.renderer.setAttribute(this.el.nativeElement, 'class', "d-none");

  // Create template elemet outside
  Promise.resolve(null).then(() => this.vcRef.createEmbeddedView(this.tpl));
}

Code de la solution, dans un contexte plus évolué de composant générique parent gérant l’aspect colonne Bootstrap : https://gist.github.com/killan/b621bf2b50e7add8b297493a05c1b949

Unwrap the kraken, ou comment faire du multicolonne avec des sous composants

Dans cet article nous parlerons d’Angular 11, de bootstrap 5, de component, de directive, de sortir le contenu d’un component au niveau du parent et d’encapsuler un contenu, le tout autour du sujet de grille bootstrap (row/col) et d’imbrications posant problème au sujet des alignements.

Vous avez surement déjà utilisé le système de grille de bootstrap et vous connaissez le principe d’un bloc dans un autre tel des poupées Russes et des Legos. Définissons alors un composant vous permettant d’afficher une zone de texte (input) précédé de son nom/étiquette (label).

Exemple d’un label suivi d’un input de type texte

En Angular nous créerons un component pour réutiliser ce système, nous n’aurons qu’à lui passer le libellé du champs, son formGroup si applicable, etc.

<mon-input label="Email" [form]="form"></mon-input>

Nous pouvons alors définir une structure qui serait par exemple :

<div class="row">
  <label class="col-4"></label>
  <input class="col-8">
</div>

Dans notre exemple nous donnons une répartition 1/3 , 2/3.

Ça c’est un cas simple mais imaginons maintenant un cas où nous aurions 2 colonnes contenant chacune le composant mon-input, suivi d’une ligne avec un composant mon-input mais qui voudrait faire la largeur des 2 colonnes.

Schéma de ce que nous voulons atteindre.

Voici ce que donnera la découpe logique une fois mis en bootstrap.

Schéma bootstrap

La première ligne donne le bon résultat : 2 colonnes chacune occupant 1/3 , 2/3 de l’espace parent. Premier problème : utiliser 1/3 , 2/3 sur la seconde ligne ne fonctionne pas.

Première idée du coup changer la répartition, et on peut essayer… ça n’ira pas. Bootstrap nous donne un conteneur row équivalent à 100% de l’espace divisé en 12 colonne théorique.

Dans notre exemple nous avons donc une coupe en 50/50 (soit 2 * 6), sans oublier qu’il y a une gouttière entre les cellule. Nous voudrions donc que la seconde ligne soit dans une répartition 1/12 ou 2/12 pour le label et le reste à l’input.

L’idée de cet article n’est pas le CSS, la mise en page en trichant ou en cassant les mécaniques rencontrées mais de faire avec naturellement et de solutionner par la construction.

Pour ne pas faire durer inutilement, la solution est que la première ligne ne doit pas être en 2 colonnes mais en une seule et de donner des 12ème de la largeur entière au 2 premiers composants. Le hic, car il y a toujours un hic, c’est qu’Angular encapsule ses composants. Au niveau du rendu en 2 colonnes vous auriez quelque chose comme :

<div class="row">
  <div class="col">
    <mon-input>
      <div class="row">
        <label class="col-4"></label>
        <input class="col-8">
      </div>
    </mon-input>
  </div>
  <div class="col">
    <mon-input>
      <div class="row">
        <label class="col-4"></label>
        <input class="col-8">
      </div>
    </mon-input>
  </div>
</div>

Et si on imagine notre solution de tout mettre sur une ligne :

<div class="row">
  <mon-input>
    <div class="row">
      <label class="col-4"></label>
      <input class="col-8">
    </div>
  </mon-input>
  <mon-input>
    <div class="row">
      <label class="col-4"></label>
      <input class="col-8">
    </div>
  </mon-input>
</div>

Il faudra également dire à notre composant de ne plus se traiter comme une ligne individuelle.

<div class="row">
  <mon-input>
    <label class="col-4"></label>
    <input class="col-8">
  </mon-input>
  <mon-input>
    <label class="col-4"></label>
    <input class="col-8">
  </mon-input>
</div>

Nous y voilà, c’est plus clair : le soucis sont les tags <mon-input>, ceux-ci considéré par Bootstrap comme des cellules sans la classe CSS adéquate (col-x). Et non, comme par magie il ne considérera pas les enfants de mon-input. De plus la répartition cumulée des col-4 et col-8 dans cette ligne dépasse 12.

Voici ce que nous voudrions, tout en ayant un composant (!) :

<div class="row">
  <mon-input></mon-input>
  <mon-input></mon-input>
</div>

Qui donnerait en interprété :

<div class="row">
  <label class="col-2"></label>
  <input class="col-4">
  <label class="col-2"></label>
  <input class="col-4">
</div>

C’est là que nous parlons de décapsuler (unwrap) notre composant pour que son host (bloc parent auto créé) ne viennent pas entraver notre volonté. La technique est simple et pourtant pas évidente à trouver, et au travers du temps, beaucoup de chose ne fonctionnent plus (ancienne version d’Angular) ou pas encore (CSS working draft ou roadmap Angular).

La solution théorique est de considérer l’intérieur de son composant comme un template

<ng-template #tref>
  <label class="col-4"></label>
  <input class="col-8">
</ng-template>

et de le cibler et le mettre en dehors avec la commande :

this.vcRef.createEmbeddedView(this.tpl);

Selon votre implémentation vous aurez peut-être comme résultat de dupliquer votre composant, une fois à l’intérieur et une fois à l’extérieur, du coup je suis parti sur une transformation de l’HTML du composant via un <ng-template #tref> et cibler celui-ci dans ma mécanique avec un :

@ViewChild('tref', { static: true }) tpl;

Ainsi dans votre ngAfterViewInit (ou ngOnInit) vous pourrez utiliser la ligne createEmbeddedView du vcRef (ViewContainerRef).

À noter qu’il faut ajouter dans @Component l’attribut host:style pour qu’il n’affecte plus le dom, car il reste présent malgré tout.

@Component({
  selector: 'mon-input',
  template: '',
  host: { style: "display: none" }
})

Vous me direz que l’on perd la notion de gouttière mais en fait non, on reste en contextes de colonnes, mais vous perdrez le cumul inutile, pour autant qu’il se marque.

Maintenant parlons de cette seconde ligne, toujours en utilisant notre composant mon-input. Vous me direz suffit de lui mettre un <div class= »row »> autour et le c’est fait. C’est pas tout à fait faux, en considérant que l’on lui envoie la nouvelle répartition des colonnes par @Input(), ce que j’ai fait chez moi, pour démo :

<div class="row">
  <mon-input label="Email" [form]="form" [distribution]="[2, 10]"></mon-input>
</div>

Et dans notre HTML nous l’utiliserions ainsi

<ng-template #tref>
  <label class="col-{{ distribution[0] }}"></label>
  <input class="col-{{ distribution[1] }}">
</ng-template>
Nouvelle répartition de la seconde ligne.

Ceci étant dit, nous ne voulons pas mettre à la main le <div class= »row »>, nous voulons que cela se fasse tout seul. Vous allez rire mais nous allons réencapsuler ce que nous venons de décapsuler. À ce stade je n’ai pas trouvé mieux pour le moment.

Pour ce faire c’est plus compliqué, étant un composant nous ne pouvons pas utiliser un sélecteur [mon-selecteur] d’un composant (component), qui se prendrait pour une directive, pour jouer sur le <ng-content> d’un template : <div class= »row »><ng-content></ng-content></div>.

Nous allons devoir utiliser une directive et ce qui est amusant c’est que la directive va appeler un composant pour « composer » notre sortie.

L’idée est que la directive considère le tag (mon-input dans notre cas) sur laquelle elle est comme le template à injecter dans le composant d’encapsulation. Ainsi, nous créons l’encapsulation, on récupère le template, on l’injecte, on peut même le paramétrer et le tour est joué. On passerait de :

<mon-input label="Email" [form]="form" [distribution]="[2, 10]" *bs-row></mon-input>

À ce type de rendu :

<bs-row-container class="row">
  <label class="col-2"></label>
  <input class="col-10">
</bs-row-container>

C’est terrible ! Exactement ce que l’on souhaite avoir.

On peut voir ici en comparaison l’ensemble.

Côté technique vous aurez la directive [bs-row] initiant l’encapsulation, et le component ‘bs-row-container’ ne contenant qu’un template rudimentaire :

@Component({
  selector: 'bs-row-container',
  template: '<ng-container *ngTemplateOutlet="tpl"></ng-container>'
})

Où tpl est renvoyé en attribut @Input par la directive.

On peut ainsi schématiser la solution finale ainsi :

<div class="row">
  <mon-input [distribution]="[2, 4]"></mon-input>
  <mon-input [distribution]="[2, 4]"></mon-input>
</div>
<mon-input [distribution]="[2, 10]" *bs-row></mon-input>

On a donc une ligne de 2 x 6 et une qui totalise 12, c’est propre, minimaliste et conforme 🙂 !


Code complet de l’encapsulation : https://gist.github.com/killan/f88e7723833bd4bb9f8798c178f5cc0b

Sources :

  • https://github.com/angular/angular/issues/7289
  • https://blog.cloudboost.io/creating-structural-directives-in-angular-ff17211c7b28
  • https://stackoverflow.com/questions/35932074/no-provider-for-templateref-ngif-templateref
  • https://stackoverflow.com/questions/41165448/building-a-wrapper-directive-wrap-some-content-component-in-angular2
  • https://stackoverflow.com/questions/60482442/multiple-components-match-node-with-tagname-app-lobby
  • https://stackblitz.com/edit/angular-eumnmt?file=src%2Fapp.ts
  • https://github.com/angular/angular/issues/18877
  • https://www.freecodecamp.org/news/everything-you-need-to-know-about-ng-template-ng-content-ng-container-and-ngtemplateoutlet-4b7b51223691/

Fusion de la chambre intermix

Suite à l’article précédent j’ai voulu faire un tour de nettoyage et je me suis rendu compte qu’un ancien démon revenait à la charge. Nous avons divisé le code par domaine et contexte de rendu, propre et héritant d’un parent commun, et dans un service je mets le code commun à ce qui concerne Grid, ou Iso etc., de manière à isoler les calculs de l’usage selon le contexte.

Schéma avant fusion

Plus simple avec un schéma, voici la découpe avant fusion. Ce qui nous intéresse c’est la séparation 2D et Gl, puis la découpe par usage/type à savoir Sprite, Grid ou Iso, puis les regroupements de type IsoElement/GridElement, ainsi que des services relatifs aux couches.

On a une sorte de matrice à 3 dimensions en ce qui concerne cette idée. Sauf que programmer ça, en TypeScript, ben c’est pas très évident. En PHP j’aurai pu utiliser des Traits, il existe des mixins en javascript mais non merci, je vous laisse vous faire votre avis mais ce n’est pas à la hauteur. L’héritage multiple n’existe pas (cf mixin), du coup il faut savoir se renouveler et faire preuve d’audace, d’expérimentation et de refontes inévitables. C’est ce que j’ai dû faire, non sans mal.

J’étais parti pour déplacer les fonctions de rendu dans les services par couche (Grid/Iso) et de fusionner Grid2DElement avec GridGlElement, vu que leur différence réside dans le contexte de rendu (2D/Gl). Mais je me suis aperçu que bien que je gagnais en clarté à tout regrouper, on augmentait d’autre part la difficulté de ce même code et des approches. Bref, bien, mais pas bien.

Du coup revirement de situation et revenons sur nos pas de plusieurs mois quand on a justement décidé de diviser par contexte de rendu, quand les lumières sont arrivées (la version solutionnée). Revenons donc à cette idée non divisée et sans emmerder les services déjà très bons tels quels.

C’est la fusion !

Fusionner Grid2D et GridGl, Iso2D et IsoGl, ok, mais on oublie Sprite2D et SpriteGl qui héritent de SpriteElement, il faut commencer par le commencement. C’est donc une refonte jusqu’à Element pour répartir les morceaux des différentes classes Sprite*. Ainsi disparaissent Sprite2DElement et SpriteGlElement au profit d’une nouvelle classe SpriteElement toute équipée.

Quand on parle de fusion on parle bien entendu de gérer le contexte de rendu au sein même de la classe. Ceci peut paraître étrange et contre certains bons principes, mais ces morceaux de code partagent parfois jusqu’à 90% du code, ce qui va contre le principe DRY (Don’t Repeat Yourself) et comme j’ai envie de bisous (KISS : Keep It Stupid Simple), j’ai tout regroupé et cela ne m’a demandé que quelque if peu coûteux, ce qui est très important car on appelle ces bouts de code des centaines, des milliers de fois par seconde (selon complexité de votre projet).

Fusion de Sprite*Element

J’en profite pour illustrer le service de rendu Gl et montrer à partir d’où on le connecte. J’en affiche un peu plus mais ainsi on voit les 2 types de regroupement GridElement et IsoElement. Ne prêtez pas attention à GridBlock, vous le connaissez déjà.

Nous sommes bien parti, continuons. On crée une nouvelle classe GridElement et on met ce que contient Grid2D et GridGl, hop tout dedans et on essaye de faire coller les morceaux. Là où ça se corse c’est de bien segmenter les parties communes et spécifiques, puis quand on arrive à IsoElement c’est encore pire car nous sommes basé sur l’héritage donc on ne réécrit que ce qui a besoin de l’être et là on observe des couacs, des oublis pour la 2D vu qu’on s’est concentré sur Gl depuis les lumières. Par exemple la table en 2D ne fonctionne pas, juste en Gl.

Fusion terminée

Sur papier ça parait plus beau, naturel, élégant, [mettre ici tous les beaux mots que vous désirez]… Mais dans la pratique ça demande pas mal d’efforts, de compréhension, évidemment c’est pour un mieux !

C’est quand même un impact de 33 fichiers dans le projet et son POC de démo, 8 dans TARS même, ainsi que 8 suppressions et 2 ajouts. Ça c’est pour les fichiers, mais en terme de lignes de code, même si je n’ai pas de compteur à cet instant, on a effectué une réduction notable, donc plus facile à maintenir, de par le regroupement aussi.

Preuve que ça fonctionne toujours, même si la table n’est pas gérée encore correctement en 2D.

Garder 2D et Gl ?

Pourquoi garder les 2 ? Car il est très difficile de débuger en Gl, vous ne pouvez pas dessiner aisément un repère ou une trace sans sortir les chars d’assaut et beaucoup d’heures de dev alors qu’en 2D vous êtes libre de manipuler le rendu en direct et ce rapidement.

C’est pour cela que le POC (démo) n’utilise plus les lumières en 2D, le but pour moi ici étant un debug rapide sans ajouter des soucis de performances, qui plus est, connus.

De plus, maintenant que la fusion est faite, on pourrait revoir tout le workflow d’utilisation pour ne plus devoir faire (à l’usage) une scène 2D ou une scène Gl. ainsi en changeant juste le mode, toutes les classes personnelles seraient utilisables, ce qui serait un chouette gain de temps et d’effort. De plus les 2 fonctions ajoutées is2DContext/isGlContext permettent d’agir spécifiquement si besoin était.

Et le fameux ensuite ?

Ah ben oui, ensuite quoi ? Corriger le pourquoi de la table en 2D et tenter de régler un conflit Grid/Iso sur un calcul de positionnement.

Il faut absolument faire un POC purement Grid et non Iso, genre un Mario (S)NES ou Duke Nukem 1-2, un truc tout carré pour voir que les calculs sont bons, juste une grille décorée, pas plus.

Ensuite, mes fameuses particules lumineuses et le miroir :p !

Mais bon, comme d’hab on verra ce qui me stitch comme on dit. Je vais déjà de ce pas fusionner les branches du repo et repartir sur une base saine :).

Edit

Pour ne pas refaire un article juste pour ça, j’ai également fusionné les classes du POC (treasure, door, player), leur code était identique. 3 classes de moins sur les 6 initiales (3x2D et 3xGl). Une bonne chose de faites qui va nous simplifier la vie ultérieurement. Les scènes restent dissociées car différentes, la Gl, plus complète, gère la lumière par exemple, les fusionner ne donnerait rien d’intéressant une fois en release. Au moins le debug (2D) peut se faire sans gêner le résultat final (Gl). Nous voilà dans un état propre, quelques corrections de bugs ou de manques seraient à faire pour solidifier avant d’avancer plus.

L’enfer est pavé de bonnes intentions

Notre premier projet étant fait, et le second ayant des besoins commun, j’ai voulu créer ma première librairie et ainsi la partager entre mes projets comme le fait NPM si je publiais. Évidemment je suis sur un git privé, pas sur Github, ni sur NPM, du coup pour le déploiement etc. ben, c’est toute une histoire…

Donc on part d’un code fonctionnel inclus dans un folder de mon app, on crée un nouveau projet de type librairie et on colle ça dedans, on nettoie le premier projet et on fait le lien entre les libs, on compile l’un et l’autre, l’impatience est à son comble ! Et ça ne va pas…

Donc on part d’une lib de référence (Material Design) et on regarde comment ils font, on s’en inspire, on prend les conventions et on observe les liens, on compile etc. Le stress est palpable ! Et ça ne va pas…

Là, le doute s’installe, malgré les lectures officielles, des articles, des mails, un stackoverflow, un appel à l’aide général et de la patience… rien n’y fit. Et la raison reste un mystère.

Que ça soit l’inclusion d’une lib en local, ou via son git perso, que ça soit un module qui ne va pas alors que l’autre oui, la folie s’installe… et surtout le rejet de ce méchant projet, ce qui le met en péril de progression.

Après avoir accepté l’échec de cette noble tentative et un gros FUCK à Angular et leur système « y a qu’les hypsters qui croivent » (comprendre : une élite peu nombreuse et souvent prétentieuse). Ô rage et désespoir… Enfin donc une idée surgit de la bonne vieille école des barbus, à l’ancienne mais restons propre.

Donc on part d’un code fonctionnel inclus dans un folder de mon app et on crée un répertoire vide en dehors et on tape le code dedans sans lui faire de mal, on ajoute un index.ts et on check que tous les liens de notre point d’entrée sont bons et accessibles.

On adapte notre projet avec le tsconfig.json et le package.json. On ajoute un script de post installation et on peut résumer ainsi : je git clone mon projet ou un nouveau et je fais le npm install, il rapatrie les node_modules nécessaires, ça on connait, il lance tout seul comme un grand le script de postinstall et va cloner notre « lib maison » sous forme de répertoire à l’intérieur de notre projet (ajouté au .gitignore), ensuite grâce aux modif du tsconfig.json, il suffit de faire une référence unique vers cette lib « comme » si c’était une lib NPM.

On respecte donc l’énoncée :

  • Chaque projet doit avoir son repo git
  • La « librairie » est facilement maintenable
  • Les projets utilisent la « librairie » facilement
  • L’ensemble ne se gêne pas

Dans le détail, dans package.json :

"postinstall": "git clone ssh://user@server.net/blabla.git src/mon-folder" 

Ajouter votre répertoire au .gitignore de votre projet :

#mon folder
/src/mon-folder

Vient ensuite votre tsconfig.json :

"baseUrl": ".",
...
"paths": {
  "@ma-lib/*": ["src/mon-folder/*"]
}

Enfin, l’usage dans votre app.module.ts :

import { MonModule } from '@ma-lib/index'; 

Un mois… ça aura pris un mois, mais on peut enfin continuer…

Et de un, maintenant à nous deux !

Ça y est, Schema Definer a été fini le week-end passé ! Le temps de redescendre du dernier rush, de valider les points et tâches dans le Redmine (système de tickets). On respire et on prend du recul.

Nous avons désormais le moyen de paramétrer notre schéma, sa définition. Nous allons pouvoir l’utiliser et en faire Folkopedia, ce qui reste le but de tout ceci. Cependant, ce serait vendre une voiture sans avoir assembler les pièces, cela reste décousu, non-défini.

Quand on fabrique un site web manipulant des données, nous utilisons des outils pour regarder à quoi ressemble les données stockées et de les manipuler. Il nous permet de savoir à quel résultat on s’attend à la sortie, et si ce n’est pas le cas d’être aidé à comprendre.

Dans notre cas spécifique ce même outil ne peut nous aider qu’à voir notre schéma et, de manière éparse, nos entités et attributs. Si l’on doit travailler avec il nous faut un outil équivalent capable d’utiliser notre schéma. Ainsi, par équivalence, nous aurons un outils d’inspection et de manipulation des entités créées.

J’ai nommé ce projet Entity Manager car le but à ce niveau est bien de travailler au niveau des entités et de leur attributs. Par extension cela concerne également leurs relations et la visibilité des contenus, ce qui couvre la totalité du schéma dans son état présent.

Voici donc un projet qui se glisse entre deux, mais qui ne ralentira que peu Folkopedia, car ce qui sera développé dans Entity Manager, telle qu’une partie de Schema Definer, pourront être mis en commun et ne pas devoir être réécrit. Cela peut paraître évident, mais pas pour tout le monde, ni rendu simple par les technologies utilisées et dans notre contexte privé.

Je vais donc techniquement partir sur une librairie Angular partagée entre nos projets qui composent Folkopedia. Pour les connaisseurs, il ne s’agit pas ici d’un projet monolithique-monorepository mais bien d’app séparée par projet ayant une lib privée en commun. La documentation étant quasi inexistante, comme par hasard, nous revoilà en quête d’impossible et de challenges d’évidences.

Links to DRY véier

Deux blocs ont été fait entièrement, les deux plus simple pour commencer et concevoir les éléments nécessaires.

Tout d’abord Visibility Level, niveau de visibilité de l’entité associée, qui a permis de mettre en place le CRUD (Create, Read, Update, Delete) requis. Ainsi sur les 5 endpoints prévu, 4 sont utiles actuellement et ont été réalisé : lister l’ensemble, ajouter, éditer et supprimer.

Pour la suppression, comme vous le savez ou l’espérez, cliquer sur le bouton ne doit pas faire l’action sans une confirmation. Généralement on vous affiche une boîte de dialogue pour vous demander de confirmer, d’une manière ou d’une autre cela vous fait regarder ailleurs ou vous masque ce vous faites.

Material Angular n’a pas de mécanique de popover tel que Bootstrap a, il n’a qu’un tooltip discret. Du coup j’en ai conçus un.

Confirmation

Alors évidemment il est personnalisable : nombre de boutons, valeur, couleur, tooltip et texte (label). Il na pas encore la fonctionnalité de position modifiable (haut, gauche, droite), il va juste dessous celui qui en a besoin, ici un bouton de suppression à confirmer.

Il verrouille le bouton cliqué, affiche sa boite élégante et attend votre clique, à côté il ferme la boute et relâche le bouton verrouillé et sur un bouton renvoie la valeur. Simple, efficace et réutilisable ! Car oui j’en ai fait un module et une directive, tout en un tant qu’on y est et qu’en plus on l’a jamais fait 🙂 mouahahaha la blague…

Vous trouverez pas mal de tuto mais aucun avec les 2 ensembles, et évidemment, aucun complet… Pensez à regarder après ‘entryComponents‘ dans votre fichier app.module.ts à l’occasion quand vous ferez ça ;).


Ceci dit, j’ai également fait le bloc Link Types, les types de lien ! 🙂 Il a une petite particularité, une variable en plus, du coup ça permet d’y aller mollo en difficulté.

Par contre les 2 suivants seront d’un coup plus complexe : défis accepté !


Il y a aussi quelques changements côté back : des tests en plus suite à l’oubli du check d’unicité durant les updates, et évidement le code de test ajouté. On a +100 tests avec ~200 comparaisons ^^ et une couverture inchangée actuellement, toujours 95% !

Autre modification back : un complément pour la liste des Link Types qui ne répondait pas parfaitement à la description de l’API. Détails, mais c’est mieux avec.

Sur le Front rouge

Après plus de douze heures à creuser ma tranchée et à établir mon avant-poste, un bivouaque, un feu et ma radio, voici enfin venu le temps de faire le compte-rendu de ce week-end.

Nous y sommes, sur le Front du combat que l’on mène depuis bientôt presque un mois, au devant du combat, dans le feux de l’Action, loin du doux cocon de l’API. Enfin, pour ceux qui ont suivi, c’était loin d’être un simple doux cocon… ah ah !

Comme prévu, à l’image de schema.org, voici l’allure de notre Schema Definer fraîchement créé. Simple et concis, de quoi parcourir les définitions et, à terme, de les éditer.

Pour faire un point technique, et justifier les 12h dites en début d’article, il a fallu commencer par mettre mon poste à jour et comme toujours non sans mal avec @angular/cli; Node ça été tout seul, et une fois le CLI (Command Line Interface) installé à la bonne version c’est Angular qui a suivi. Nous voilà donc avec un environnement propre Angular 8 et Material Design pour le fun et l’inconnue.

Pour ceux que ça intéresse, faites gaffe avec Material Design, cela ne remplace pas Bootstrap (grille, typo, style etc), mais ne concerne que les composants (bouton, tableau, formulaire, …). En bref, faites attention.

S’en est suivis une mise en place du projet et de l’authentification avec service et gardien (guard). Je vous passe les emmerdes et vous redirige vers quelques articles qui m’ont bien aidé/inspiré.

La liste n’est pas exhaustive. C’est surtout le premier qui m’a bien aidé à comprendre la mise en place de la mécanique du gardien en Angular, ainsi que les interceptor et quelques opérateurs rxjs.

Vu la progression du week-end, voici un extrait représentatif du travail accompli depuis le début du mois (le détail serait beaucoup trop long), et qui, ce week-end, a fait un beau bon en avant.

Maintenant que la machine est en place, le reste devrait suivre sans nouvelle mauvaise surprise. Enfin, j’espère…