REST API avec Node/Express + OpenAPI

Pas de révolution ici, c’est quelque chose somme toute d’assez classique, un back-end NodeJs en Express et une doc Swagger basé sur un OpenAPI. Le truc étant de faire le routage et la doc en un seul point, améliorant la maintenabilité et automatisant le routage.

Pour ce faire il suffit de démarrer un nouveau projet et d’installer quelques librairies de base :

npm init
npm i express --save
npm i swagger-parser --save
npm i cors --save
npm i swagger-routes-express --save
  • Le framework express comme base,
  • La lib swagger-parser pour lire et interpréter le fichier openapi.yaml (définition de l’API),
  • La lib cors pour définir l’autorisation,
  • La lib swagger-routes-express pour créer un connecteur reliant les contrôleurs au routeur basé sur la définition de l’API,
  • La lib swagger-ui-express pour mettre à disposition un swagger de la définition de l’API, sur une route /api-docs.

Maintenant qu’on a la base il nous faut le squelette d’application, du coup au lieu de taper tout le code ici je t’invite cher lecteur à te rendre sur ce GitHub, et nous allons détailler les parties intéressantes.

OpenAPI

D’abord, qu’est-ce que l’on veut accomplir ? Dans cet exemple on va simplement faire un service REST pour obtenir une liste d’items, un GET. Nous allons donc décrire un fichier openapi.yaml décrivant cela.

openapi: 3.0.0
info:
  description: service backend
  version: 1.0.0
  title: my-api
paths:
  /items:
    get:
      summary: Get all items
      description: Get all items
      operationId: getItems
      responses:
        "200":
          description: success
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/item'
servers:
  - url: /api/v1
components:
  schemas:
    item:
      type: object
      properties:
        id:
          type: number
        name:
          type: string
        date:
          type: string

En gros : on définit une route /items qui renverra un tableau d’item et la structure item. Jusque là c’est du formalisme standard que vous retrouverez un peu partout.

Le serveur

Ensuite il faut construire notre serveur (je n’utiliserai pas le générateur), pour cela nous aurons principalement 2 fichiers de base et un fichier contrôleur. Le schéma étant assez simple, on reçoit une requête, le routeur fait un match et associe la méthode du contrôleur.

On commence par écrire un fichier makeApp.js (dans /src), qui correspond à la définition de notre serveur, notre application. Dedans on y trouve beaucoup de choses, on y reviendra plus en détail après, mais en gros on décrit ce que nous avons dit plus haut, à savoir : définir un serveur express auquel on va connecter le routeur, lui-même basé sur le fichier openapi.yaml décrit plus haut. Le connecteur du routeur se lie aux contrôleurs, nous y reviendrons.

const express = require('express')
const SwaggerParser = require('swagger-parser')
const swaggerRoutes = require('swagger-routes-express')
const ctrls = require('./controllers')
const cors = require("cors")

const makeApp = async () => {
  const parser = new SwaggerParser()
  const apiDescription = await parser.validate('./api/openapi.yaml')
  const connect = swaggerRoutes.connector(ctrls, apiDescription)
  const app = express()

...

  // Connect the routes
  connect(app)

  // Add any error handlers last

  return app
}
module.exports = makeApp

Ensuite on a le fichier point de départ : index.js, que l’on placera dans un répertoire /src, du coup n’oubliez pas de modifier package.json avec la propriété main :

"main": "./src/index",

Ainsi que la commande start, celle ci se lancerait avec un npm run start mais vous pourriez avoir une surprise, via un Git Bash sur Windows, d’avoir une erreur sur le ./

  "scripts": {
    "start": "./src/index.js",

Dans ce fichier index.js on aura un appel au précédent fichier de config.

const makeApp = require('./makeApp')
const port = 3000;

makeApp()
  .then(app => app.listen(port))
  .then(() => {
    console.log(`App running on port ${port}...`)
  })
  .catch(err => {
    console.error('caught error', err)
  })

Le contrôleur

Nous avons vu que le serveur se lie aux contrôleurs, il est maintenant temps de nous y intéresser. Nous allons créer un répertoire controllers dans /src, et un fichier item.js permettant de regrouper par domaine les actions concernant les items. À côté on créer un fichier index.js permettant de lister le contenu du répertoire au niveau de l’appelant, une façon de faire également utilisée en typescript/Angular.

Le fichier item.js

exports.getItems = async (req, res, next) => {
  res.json([]);
};

Le fichier index.js

const { getItems } = require('./item')

module.exports = {
  getItems
}

On peut voir la fonction getItems dont le nom match l’attribut operationId du fichier openapi.yaml. Évidemment c’est voulu 🙂 Et c’est important de garder ça à l’œil quand vous préparez votre YAML. Actuellement la méthode renvoie un tableau vide.

Compléter le serveur

Avant de vouloir tester il nous manque un morceau, en fait plusieurs petits détails. Pour faire des appels à notre API, depuis notre poste, on aura un soucis de CORS, mais aussi, potentiellement de cache (en-tête etag) et d’url encoding. À cela on veut préciser que l’on traitera du JSON dans les échanges.

  // Options
  app.set("etag", false); //turn off
  app.use(
    express.urlencoded({
      extended: true,
    })
  );

  app.use(express.json());

  app.use(cors({
    origin: '*'
  }))

Tester

Pour tester on peut lancer le serveur avec : node src/index.js

On peut ouvrir un Bash et lancer un curl de test, tel que :

$ curl -s localhost:3000/api/v1/items
[]

Du coup bonne nouvelle on a quelque chose qui fonctionne et répond correctement, à savoir une réponse d’un tableau vide ([]). Pour le fun on peut structurer une donnée et la renvoyer, en respectant le modèle proposé dans le YAML.

exports.getItems = async (req, res, next) => {
  res.json([
    {
      id: 1,
      name: "Item 1",
      date: "2022-07-13"
    },
    {
      id: 2,
      name: "Item 2",
      date: "2022-07-01"
    }
  ]);
};
$ curl -s localhost:3000/api/v1/items
[{"id":1,"name":"Item 1","date":"2022-07-13"},{"id":2,"name":"Item 2","date":"2022-07-01"}]

Ajouter la documentation Swagger

D’abord il va nous falloir une lib en plus :

npm i swagger-ui-express --save

Ensuite on va ajouter les lignes suivantes dans notre makeApp.js, l’une dans les déclarations, l’autre après les options afin de déclarer une route de documentation et lancer Swagger.

const express = require('express')
const swaggerUi = require("swagger-ui-express");

...

  // This is the endpoint that will display the swagger docs
  app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(apiDescription));

Couper/relancer le serveur et rendez-vous sur l’url localhost:3000/api-docs.

Illustration du résultat du Swagger

Et ensuite ?

À partir de là libre à vous de créer des services, connecter une base de données, gérer du fichier, etc.

Liste des ressources utilisées

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 🙂