Une autre façon d'écrire un composant Angular avec Analog

Une autre façon d'écrire un composant Angular avec Analog

Loïc Boutter (lazybobcat)
Loïc Boutter (lazybobcat)

N’avez-vous jamais trouvé l’écriture des composants Angular très répétitive et très verbeuse ? Moi oui. Le décorateur @Component({}) par exemple est très long et je mets toujours la même chose dedans : un sélecteur unique à mon composant, la stratégie de détection de changement OnPush, standalone: true et un parfois un peu de CSS.

Si seulement tout cela pouvait être fait par défaut, sans avoir besoin ni de l’écrire ni de le voir, eh bien, on peut !


Sommaire

  1. C’est quoi Analog ?
  2. Comment Analog propose d’écrire les composants ?
  3. Et si j’ai des configurations à passer au décorateur @Component ?
  4. Utiliser les composants
  5. N’importez plus vos composants standalone
  6. Cycle de vie du composant
  7. Utiliser du Markdown dans le composant
  8. Installation d’Analog
  9. Liens

C’est quoi Analog ?

Si vous n’avez pas encore entendu parler d’Analog, je vous suggère de garder un oeil dessus à l’avenir. Il s’agit d’un “meta-framework” pour Angular, permettant d’effectuer certaines tâches beaucoup plus rapidement. Par exemple, des routes peuvent être générées automatiquement en fonction de l’arborescence de certains fichiers, il est possible d’écrire du contenu directement en markdown, supporte le SSR par défaut et bien d’autres !

Ce chapitre vous expliquera comment installer Analog en fonction de votre situation. Mais comme ce n’est pas le sujet principal de cet article, passons plutôt au vif du sujet…

Comment Analog propose d’écrire les composants ?

Format

L’idée est de créer un nouveau format de fichier, .analog, permettant de fournir tout le code dont a besoin un composant (code Typescript, template HTML, styles et méta-données) dans un seul fichier avec le moins d’informations superflues possibles. Analog appelle ce type de composant un “SFC” (pour Single File Component). Je les appellerai dans cet article “composants Analog” ou “composants SFC”.

Ce format de fichier est expérimental et des choses peuvent changer et des fonctionnalités peuvent s’ajouter.

Afin de ne pas avoir un fichier trop long, certaines configurations ont été choisies par défaut : la stratégie de détection de changement est OnPush (et c’est très bien) et l’accès à certaines méthodes du cycle de vie du composant ont été retirées (pas de panique, elles ne sont plus vraiment nécessaire avec OnPush et les signaux).

De plus, on ne déclare plus notre composant au sein d’une classe mais on utilise de simples variables et fonctions. Par exemple, un simple bouton qui compte le nombre de fois qu’il a été cliqué est implémenté ainsi :

counter.analog
<script lang="ts">
  import { signal } from '@angular/core';
 
  const count = signal(0);
 
  function add() {
    count.set(count() + 1);
  }
</script>
 
<template>
  <div class="container">
    <button (click)="add()">{{count()}}</button>
  </div>
</template>
 
<style>
  .container {
    display: flex;
    justify-content: center;
  }
 
  button {
    font-size: 2rem;
    padding: 1rem 2rem;
    border-radius: 0.5rem;
    background-color: #f0f0f0;
    border: 1px solid #ccc;
  }
</style>

Décomposons ce fichier :

  • Nous déclarons d’abord un <script> qui contient en fait le code Typescript de notre composant. Rien de superflu ici, seulement le code nécessaire au fonctionnement et les différents imports.
  • Nous déclarons ensuite notre <template>, ici rien ne change par rapport à ce que vous faites habituellement.
  • Enfin, le <style> permet de fournir le style nécessaire au bon affichage de notre composant

Notons que le style est facultatif, comme auparavant. Si aucun <template> n’est fourni alors votre SFC sera considéré comme une Directive plutôt qu’un Component.

Comment fait Analog ?

En réalité c’est très simple, Analog transpile votre fichier .analog en un composant Angular standard en récupérant ce qui se trouve dans <script>, <template> et <style> et en les plaçant astucieusement où il faut dans le composant ! Il fallait y penser.

Et si j’ai des configurations à passer au décorateur @Component ?

Pas de panique, une fonction defineMetadata() est mise à disposition par Analog permettant de déclarer toutes les options disponibles dans les décorateurs @Component({}) et @Directive({}) à l’exception de :

  • template : eh bien oui, le template est déclaré dans le fichier .analog
  • styles : pour la même raison que template :)
  • standalone : tous les SFCs sont standalone: true
  • changeDetection : la stratégie est toujours à OnPush
  • outputs et inputs : vous devriez utiliser les fonctions d’Angular associés aux signaux de toute façon !

Même si, template et styles ne sont pas disponibles, vous pouvez tout de même utiliser un template ou des styles dans leurs propres fichiers en utilisant templateUrl et styleUrl :

root.analog
<script lang="ts">
  defineMetadata({
    selector: 'app-root',
    templateUrl: './test.html',
    styleUrl: './test.css',
  });
</script>

Utiliser les composants

Très bien, nous savons désormais comment écrire un composant “SFC”, mais comment l’inclure dans un autre composant, comment l’utiliser ?

Ce n’est pas plus compliqué qu’un autre composant. Analog exporte par défaut le composant à partir de votre fichier .analog. Vous pouvez donc l’importer en lui donnant le nom que vous souhaitez, par exemple :

import Counter from './components/counter.analog';
// ou
import MySuperCounterComponent from './components/counter.analog';

Si vous souhaitez bootstraper un composant Analog comme composant principal de l’application :

main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import App from './app/app.analog';
import { appConfig } from './app/app.config';
 
bootstrapApplication(App, appConfig).catch((err) => console.error(err));

Si vous souhaitez utiliser un composant Analog dans un autre composant :

home.component.ts
import { Component } from '@angular/core';
import Counter from '../components/counter.analog';
 
@Component({
  selector: 'app-home',
  standalone: true,
  imports: [Counter],
  template: `
    <Counter />
  `,
})
export default class HomeComponent {
}

Notez que le sélecteur par défaut est le nom du fichier en PascalCase (et sans l’extension). Donc dans l’exemple ci-dessus, pour notre fichier counter.analog, le sélecteur est Counter. Vous pouvez bien sûr changer le sélecteur à l’aide de la fonction defineMetadata() !

N’importez plus vos composants standalone

Si vous importez un composant standalone ou un service dans un autre composant Analog, vous n’avez pas besoin de les ajouter dans le import de la fonction defineMetadata(), à condition d’utiliser l’attribut d’import with :

<script lang="ts">
  // Ajout du composant standalone dans "imports" :
  import CounterComponent from './components/counter.analog' with { analog: 'imports' };
  // Ajout du service aux "providers" :
  import { CounterStore } from './services/counter.store' with { analog: 'providers' };
  // Ajout d'une classe au constructeur, en public, afin qu'il soit exposé au template :
  import { CounterTypeEnum } from './enums/counter-type.enum' with { analog: 'exposes' };
</script>

Cycle de vie du composant

Angular met à disposition du développeur des méthodes appelées tout au long du cycle de vie du composant.

Pour le moment, seules deux de ces fonctions sont disponibles avec Analog : onInit() et onDestroy(). L’utilisation de ces fonction est très simple, on leur fourni une fonction de “callback” qui sera appelée lorsque le composant sera initialisé et détruit :

articles.analog
<script lang="ts">
  onInit(() => {
    store.loadArticles();
  });
 
  onDestroy(() => {
    console.log('Goodbye!');
  });
</script>

Utiliser du Markdown dans le composant

C’est tellement enfantin que ça en devient déconcertant. Tout ce que vous avez à faire c’est de qualifier <template> avec lang="md". Vous pouvez utiliser toutes les variables déclarées dans la partie <script> dans le contenu Markdown, ainsi que d’autres composants :

documentation.analog
<script lang="ts">
  import Counter from './counter.analog' with { analog: 'imports' };
 
  const name = "MySuperLib";
</script>
 
<template lang="md">
  # Documentation de {{ name }}
 
  [This is a link to nowhere](#) and this text is **bold** or *italic*.
 
  <Counter />
</template>

Et voilà, juste comme ça votre Markdown est transformé en HTML. Si vous rêviez d’un blog avec Angular où il est très facile d’écrire vos articles en Markdown, cela pourra vous être utile. Vous pouvez même aller plus loin car Analog propose du contenu interactif avec frontmatter.

Installation d’Analog

Nouveau projet Analog

Pour démarrer votre projet Analog, éxecutez :

npm create analog@latest
// Ou, en utilisant Nx :
npx create-nx-workspace@latest --preset=@analogjs/platform
 
// Ajouter Analog à un workspace Nx existant :
npm install @analogjs/platform --save-dev
npx nx g @analogjs/platform:app analog-app
 
// Lancer l'application
npm run start
// Ou, avec Nx :
npx nx serve analog-app

Pour plus d’informations sur l’installation et les fonctionnalités d’Analog, faites un tour sur la documentation. Il est également possible que j’écrirai des articles sur d’autres fonctionnalités d’Analog à l’avenir.

Ajouter Analog à un projet Angular existant

Premièrement, il faut que votre version d’Angular soit supérieure ou égale à la version 17.1. Il faut ensuite installer un builder ESBuild custom ainsi que le plugin “Analog Vite” pour Angular :

npm i -D @angular-builders/custom-esbuild @analogjs/vite-plugin-angular

Vous pourrez ensuite modifier le builder dans le fichier angular.json (ou project.json si vous utilisez Nx) et ajouter ou modifier les options suivantes :

angular.json
- "builder": "@angular-devkit/build-angular:application",
+ "builder": "@angular-builders/custom-esbuild:application",
+ "options": {
+     "outputPath": "dist/angular-esbuild-analog-example",
+     "scripts": [],
+     "plugins": ["./esbuild/plugins.js"]
+  },

Et pour le serveur de développement :

angular.json
- "builder": "@angular-devkit/build-angular:dev-server",
+ "builder": "@angular-builders/custom-esbuild:dev-server",

Créez un dossier esbuild à la racine du projet contenant un fichier package.json et un fichier plugins.js.

├── esbuild
│   ├── plugins.js
│   ├── package.json
└── angular.json

Dans le fichier package.json, ajouter { "type": "module" }.

Dans le fichier plugins.js ajouter activez le plugin Analog SFC:

import { analogSFC } from '@analogjs/vite-plugin-angular/esbuild';
 
export default [analogSFC()];

Enfin, créez un fichier analog.d.ts dans le dossier src d’Angular. Ce fichier est temporaire, le temps que le nouveau format de fichier est expérimental :

interface ImportAttributes {
  analog: 'imports' | 'providers' | 'viewProviders' | 'exposes';
}
 
declare global {
  import type { Component } from '@angular/core';
 
  interface Window {
    /**
     * Define the metadata for the component.
     * @param metadata
     */
    defineMetadata: (
      metadata: Omit<
        Component,
        | 'template'
        | 'standalone'
        | 'changeDetection'
        | 'styles'
        | 'outputs'
        | 'inputs'
      >
    ) => void;
 
    /**
     * Invoke the callback when the component is initialized.
     */
    onInit: (initFn: () => void) => void;
    /**
     * Invoke the callback when the component is destroyed.
     */
    onDestroy: (destroyFn: () => void) => void;
  }
}
 
declare module '*.analog' {
  import { Type } from "@angular/core";
 
  const cmp: Type<any>;
  export default cmp;
}

C’est bon, vous êtes prêt à utiliser Analog. Pour plus d’informations, consultez cet article écrit par nul autre que Brandon Roberts, le créateur d’Analog.

Liens et crédits

Et un remerciement à Brandon Roberts pour avoir répondu à mes tweets lors de la rédaction de cet article !

Retour aux articles