Comment créer et déployer une application de matériau angulaire

Publié: 2022-03-10
Résumé rapide ↬ Une procédure pas à pas pour créer une application Web Angular 8 et une application de génération de code QR entièrement basées sur Angular tout en étant hébergées sur Netlify.

Angular est l'un des choix populaires lors de la création de nouvelles applications Web. De plus, les spécifications "Material Design" sont devenues un choix incontournable pour créer une expérience minimale et engageante aujourd'hui. Ainsi, tout nouveau projet "Angular" utilise principalement la "Bibliothèque de conception de matériaux angulaires" pour utiliser les composants qui suivent les spécifications de conception de matériaux. Des animations fluides aux retours d'interaction appropriés, tout cela est déjà disponible dans le cadre de la bibliothèque officielle de conception de matériaux pour angular.

Une fois l'application Web développée, l'étape suivante consiste à la déployer. C'est là que "Netlify" entre en scène. Avec son interface très facile à utiliser, son déploiement automatique, sa répartition du trafic pour les tests A/B et diverses autres fonctionnalités, Netlify est sûrement un excellent outil.

L'article sera une présentation de la création d'une application Web Angular 8 à l'aide de la bibliothèque officielle Angular Material Design. Nous allons créer une application Web de générateur de code QR entièrement basée sur Angular tout en étant hébergée sur Netlify.

Les fichiers de ce didacticiel sont disponibles sur GitHub et une version de démonstration est déployée ici.

Commencer

  1. Installer angulaire 8,
  2. Créez un compte GitHub,
  3. Installez Git sur votre ordinateur,
  4. Créez un compte Netlify.

Remarque : J'utiliserai VSCode et Microsoft Windows comme IDE et système d'exploitation préférés, bien que les étapes soient similaires pour tout autre IDE sur tout autre système d'exploitation.

Une fois les prérequis ci-dessus remplis, commençons !

Plus après saut! Continuez à lire ci-dessous ↓

Simulations et planification

Avant de commencer à créer le projet, il serait utile de planifier à l'avance : quel type d'interface utilisateur voudrions-nous dans notre application ? Y aura-t-il des pièces réutilisables ? Comment l'application va-t-elle interagir avec les services externes ?

Tout d'abord, vérifiez les simulations d'interface utilisateur.

Page d'accueil ( Grand aperçu )
Création d'une page QR ( Grand aperçu )
Page Historique ( Grand aperçu )

Ce sont les trois pages différentes qui seront contenues dans l'application. La page d'accueil sera le point de départ de notre application. La création d'une page QR doit traiter de la création d'un nouveau code QR. La page Historique affichera tous les codes QR enregistrés.

Les maquettes donnent non seulement une idée de l'apparence de l'application, mais elles séparent également la responsabilité de chaque page.

Une observation (d'après les simulations) est qu'il semble que la barre de navigation supérieure soit commune à toutes les pages. Ainsi, la barre de navigation peut être créée en tant que composant réutilisable et réutilisée.

Maintenant que nous avons une bonne idée de l'apparence de l'application et de ce qui peut être réutilisé, commençons.

Créer un nouveau projet angulaire

Lancez VSCode, puis ouvrez une fenêtre de terminal dans VSCode pour générer un nouveau projet Angular.

Terminal dans VSCode ( Grand aperçu )

Le terminal s'ouvrira avec un chemin par défaut, comme indiqué dans l'invite. Vous pouvez passer à un répertoire préféré avant de continuer ; dans le cas de Windows, j'utiliserai la commande cd .

Naviguer vers le chemin préféré ( Grand aperçu )

À l'avenir, angular-cli a une commande pour générer de nouveaux projets avec le ng new <project-name> . Utilisez simplement n'importe quel nom de projet fantaisiste que vous aimez et appuyez sur Entrée, par exemple ng new qr .

Cela déclenchera la magie angular-cli; il fournira quelques options pour configurer certains aspects du projet, par exemple, l'ajout d'un routage angulaire. Ensuite, en fonction des options sélectionnées, il générera l'ensemble du squelette du projet qui pourra être exécuté sans aucune modification.

Pour ce didacticiel, entrez Oui pour le routage et sélectionnez CSS pour le style. Cela va générer un nouveau projet Angular :

Création d'un nouveau projet angulaire ( Grand aperçu )

Nous avons maintenant un projet angulaire entièrement fonctionnel. Afin de nous assurer que tout fonctionne correctement, nous pouvons exécuter le projet en entrant cette commande dans le terminal : ng serve . Euh oh, mais attendez, cela entraîne une erreur. Qu'est-ce qui aurait pu se passer ?

erreur de service ng ( Grand aperçu )

Ne t'inquiète pas. Chaque fois que vous créez un nouveau projet à l'aide d'angular-cli, il génère l'ensemble du squelette dans un dossier nommé d'après le nom du projet spécifié dans la commande ng new qr . Ici, nous devrons changer le répertoire de travail actuel pour celui qui vient d'être créé. Sous Windows, utilisez la commande cd qr pour changer de répertoire.

Maintenant, essayez de relancer le projet avec l'aide de ng serve :

Projet en cours ( Grand aperçu )

Ouvrez un navigateur Web, accédez à l'URL https://localhost:4200 pour voir le projet en cours d'exécution. La commande ng serve exécute l'application sur le port 4200 par défaut.

ASTUCE : Pour l'exécuter sur un port différent, nous utilisons la commande ng serve --port <any-port> par exemple, ng serve --port 3000 .

Cela garantit que notre projet angulaire de base est opérationnel. Allons-nous en.

Nous devons ajouter le dossier du projet à VSCode. Allez dans le menu "Fichier" et sélectionnez "Ouvrir le dossier" et sélectionnez le dossier du projet. Le dossier du projet sera maintenant affiché dans la vue Explorateur sur la gauche.

Ajout d'une bibliothèque de matériaux angulaires

Pour installer la bibliothèque de matériaux Angular, utilisez la commande suivante dans la fenêtre du terminal : ng add @angular/material . Cela posera (encore) quelques questions telles que le thème que vous voulez, si vous voulez des animations par défaut, si le support tactile est requis, entre autres. Nous allons simplement sélectionner le thème Indigo/Pink par défaut, Yes pour ajouter la bibliothèque HammerJS et les animations du navigateur.

Ajout de matériau angulaire ( Grand aperçu )

La commande ci-dessus configure également l'ensemble du projet pour activer la prise en charge des composants matériels.

  1. Il ajoute des dépendances de projet à package.json ,
  2. Il ajoute la police Roboto au fichier index.html ,
  3. Il ajoute la police d'icônes Material Design à votre index.html ,
  4. Il ajoute également quelques styles CSS globaux à :
    • Supprimer les marges du corps,
    • Définir la height: 100% dans le HTML et le corps,
    • Définissez Roboto comme police d'application par défaut.

Juste pour être sûr que tout va bien, vous pouvez relancer le projet à ce stade, même si vous ne remarquerez rien de nouveau.

Ajout de la page d'accueil

Notre squelette de projet est maintenant prêt. Commençons par ajouter la page d'accueil.

( Grand aperçu )

Nous voulons garder notre page d'accueil simple, tout comme l'image ci-dessus. Cette page d'accueil utilise quelques composants matériels angulaires. Disséquons.

  1. La barre supérieure est un simple élément de nav HTML qui contient un bouton de style de matériau, mat-button , avec une image et un texte comme enfant. La couleur de la barre est la même que la couleur primaire qui a été sélectionnée lors de l'ajout de la bibliothèque de matériaux angulaires ;
  2. Une image centrée ;
  3. Un autre, mat-button , avec juste un texte comme enfant. Ce bouton permettra aux utilisateurs de naviguer vers la page d'historique ;
  4. Un badge de comptage, matBadge , attaché au bouton ci-dessus, indiquant le nombre de QR codes enregistrés par l'utilisateur ;
  5. Un bouton d'action flottant, mat-fab , dans le coin inférieur droit ayant la couleur d'accent du thème sélectionné.

En nous écartant un peu, ajoutons d'abord les autres composants et services requis.

Ajout d'en-tête

Comme prévu précédemment, la barre de navigation doit être réutilisée, créons-la en tant que composant angulaire séparé. Ouvrez le terminal dans VSCode et tapez ng gc header (abréviation de ng generate component header) et appuyez sur Entrée. Cela créera un nouveau dossier nommé "header" qui contiendra quatre fichiers :

  • header.component.css : utilisé pour donner un style à ce composant ;
  • header.component.html : pour ajouter des éléments HTML ;
  • header.component.spec.ts : pour écrire des cas de test ;
  • header.component.ts : pour ajouter la logique basée sur Typescript.
Composant d'en-tête ( Grand aperçu )

Pour que l'en-tête ressemble à ce qu'il était dans les maquettes, ajoutez le code HTML ci-dessous dans header.component.html :

 <nav class="navbar" [class.mat-elevation-z8]=true> <div> <button *ngIf="showBackButton" aria-hidden=false mat-icon-button routerLink="/"> <mat-icon> <i class="material-icons md-32">arrow_back</i> </mat-icon> </button> <span>{{currentTitle}}</span> </div> <button *ngIf="!showBackButton" aria-hidden=false mat-button class="button"> <img src="../../assets/qr-icon-white.png"> <span>QR Generator</span> </button> <button *ngIf="showHistoryNav" aria-hidden=false mat-button class="button" routerLink="/history"> <span>History</span> </button> </nav>

ASTUCE : Pour ajouter une élévation à n'importe quel composant de matériau, utilisez [class.mat-elevation-z8]=true , la valeur d'élévation peut être modifiée en modifiant la valeur z , dans ce cas, il s'agit de z8 . Par exemple, pour changer l'élévation à 16, utilisez [class.mat-elevation-z16]=true .

Dans l'extrait de code HTML ci-dessus, deux éléments de matériau angulaire sont utilisés : mat-icon et mat-button/mat-icon-button . Leur utilisation est très simple ; d'abord, nous devons ajouter ces deux en tant que modules dans notre app.module.ts comme indiqué ci-dessous :

Importation de module pour mat-icon et mat-button ( Grand aperçu )

Cela nous permettra d'utiliser ces deux éléments matériels angulaires n'importe où dans n'importe quel composant.

Pour ajouter des boutons de matériau, l'extrait de code HTML suivant est utilisé :

 <button mat-button> Material Button </button>

Il existe différents types d'éléments de bouton de matériau disponibles dans la bibliothèque de matériaux angulaires, tels que mat-raised-button , mat-flat-button , mat-fab et autres; remplacez simplement le mat-button dans l'extrait de code ci-dessus par tout autre type.

Types de boutons matériels ( Grand aperçu )

L'autre élément est mat-icon qui est utilisé pour afficher les icônes disponibles dans la bibliothèque d'icônes de matériaux. Lorsque la bibliothèque de matériaux angulaires a été ajoutée au début, une référence à la bibliothèque d'icônes de matériaux a également été ajoutée, ce qui nous a permis d'utiliser des icônes parmi la vaste gamme d'icônes.

L'utilisation est aussi simple que :

 <mat-icon> <i class="material-icons md-32">arrow_back</i> </mat-icon>

La balise imbriquée <i> peut être utilisée pour changer la taille de l'icône (ici c'est md-32 ) ce qui rendra la taille de l'icône de 32 pixels en hauteur et en largeur. Cette valeur peut être md-24 , md-48 , etc. La valeur de la balise imbriquée <i> est le nom de l'icône. (Le nom peut être trouvé ici pour toute autre icône.)

Accessibilité

Chaque fois que des icônes ou des images sont utilisées, il est impératif qu'elles fournissent des informations suffisantes à des fins d'accessibilité ou pour un utilisateur de lecteur d'écran. ARIA (Accessible Rich Internet Applications) définit un moyen de rendre le contenu Web et les applications Web plus accessibles aux personnes handicapées.

Un point à noter est que les éléments HTML qui ont leur sémantique native (par exemple nav ) n'ont pas besoin d'attributs ARIA ; le lecteur d'écran saura déjà que nav est un élément de navigation et le lira comme tel.

Les spécifications ARIA sont divisées en trois catégories : rôles, états et propriétés. Disons qu'un div est utilisé pour créer une barre de progression dans le code HTML. Il n'a pas de sémantique native ; Le rôle ARIA peut décrire ce widget comme une barre de progression, la propriété ARIA peut indiquer sa caractéristique telle qu'elle peut être glissée. L'état ARIA décrira son état actuel tel que la valeur actuelle de la barre de progression. Voir l'extrait ci-dessous :

 <div role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100"> </div>

De même, un attribut aria très couramment utilisé : aria-hidden=true/false est utilisé. La valeur true rend cet élément invisible pour les lecteurs d'écran.

Étant donné que la plupart des éléments de l'interface utilisateur utilisés dans cette application ont une signification sémantique native, les seuls attributs ARIA utilisés sont pour spécifier les états de visibilité ARIA. Pour des informations détaillées, reportez-vous à ceci.

Le header.component.html contient une certaine logique pour masquer et afficher le bouton de retour en fonction de la page actuelle. De plus, le bouton Accueil contient également une image/logo qui doit être ajouté au dossier /assets . Téléchargez l'image à partir d'ici et enregistrez-la dans le dossier /assets .

Pour le style de la barre de navigation, ajoutez le css ci-dessous dans header.component.css :

 .navbar { position: fixed; top: 0; left: 0; right: 0; z-index: 2; background: #3f51b5; display: flex; flex-wrap: wrap; align-items: center; padding: 12px 16px; } .button { color: white; margin: 0px 10px; }

Comme nous voulons garder le composant d'en-tête réutilisable dans d'autres composants, donc pour décider ce qui doit être affiché, nous aurons besoin de ceux-ci en tant que paramètres d'autres composants. Cela nécessite l'utilisation du décorateur @Input() qui se liera aux variables que nous avons utilisées dans header.component.html .

Ajoutez ces lignes dans le fichier header.component.ts :

 // Add these three lines above the constructor entry. @Input() showBackButton: boolean; @Input() currentTitle: string; @Input() showHistoryNav: boolean; constructor() { }

Les trois liaisons ci-dessus seront transmises en tant que paramètre à partir d'autres composants que le composant d'en-tête utilisera. Son utilisation sera plus claire une fois que nous aurons avancé.

Ensuite, nous devons créer une page d'accueil qui peut être représentée par un composant angulaire. Commençons donc par créer un autre composant ; tapez ng gc home dans le terminal pour générer automatiquement le composant home. Comme précédemment, un nouveau dossier nommé "home" sera créé contenant quatre fichiers différents. Avant de procéder à la modification de ces fichiers, ajoutons quelques informations de routage au module de routage angulaire.

Ajout de routage

Angular fournit un moyen de mapper l'URL à un composant spécifique. Chaque fois qu'une navigation se produit, le framework Angular surveille l'URL et se base sur les informations présentes dans le fichier app-routing.module.ts ; il initialise le composant mappé. De cette façon, différents composants n'ont pas besoin d'assumer la responsabilité d'initialiser d'autres composants. Dans notre cas, l'application comporte trois pages navigables en cliquant sur différents boutons. Nous y parvenons en tirant parti du support de routage fourni par le framework Angular.

Le composant home doit être le point de départ de l'application. Ajoutons ces informations au fichier app-routing.module.ts .

Composant d'accueil de routage ( Grand aperçu )

La propriété path est définie comme une chaîne vide ; cela nous permet de mapper l'URL de l'application au composant de la page d'accueil, quelque chose comme google.com qui affiche la page d'accueil de Google.

ASTUCE : la valeur du chemin ne commence jamais par un « / », mais utilise à la place une chaîne vide même si le chemin peut ressembler à search/coffee .

En revenant au composant de la page d'accueil, remplacez le contenu de home.component.html par ceci :

 <app-header [showBackButton]="false" [currentTitle]=""></app-header> <app-profile></app-profile> <!-- FAB Fixed --> <button mat-fab class="fab-bottom-right" routerLink="/create"> <mat-icon> <i class="material-icons md-48">add</i> </mat-icon> </button>

Il y a trois parties dans le composant home :

  1. Le composant d'en-tête réutilisable <app-header> ,
  2. Composant de profil <app-profile> ,
  3. Le bouton d'action flottant en bas à droite.

L'extrait de code HTML ci-dessus montre comment le composant d'en-tête réutilisable est utilisé dans d'autres composants ; nous utilisons simplement le sélecteur de composants et passons les paramètres requis.

Le composant de profil est créé pour être utilisé comme corps de la page d'accueil - nous le créerons bientôt.

Le bouton d'action flottant avec l'icône + est une sorte de bouton de matériau angulaire de type mat-fab en bas à droite de l'écran. Il a la directive d'attribut routerLink qui utilise les informations d'itinéraire fournies dans app-routing.module.ts pour la navigation. Dans ce cas, le bouton a la valeur de route comme /create qui sera mappé pour créer un composant.

Pour faire flotter le bouton de création en bas à droite, ajoutez le code CSS ci-dessous dans home.component.css :

 .fab-bottom-right { position: fixed; left: auto; bottom: 5%; right: 10%; }

Étant donné que le composant de profil est censé gérer le corps de la page d'accueil, nous laisserons home.component.ts intact.

Ajout d'un composant de profil

Ouvrez le terminal, tapez ng gc profile et appuyez sur Entrée pour générer le composant de profil. Comme prévu précédemment, ce composant gérera le corps principal de la page d'accueil. Ouvrez profile.component.html et remplacez son contenu par ceci :

 <div class="center profile-child"> <img class="avatar" src="../../assets/avatar.png"> <div class="profile-actions"> <button mat-raised-button matBadge="{{historyCount}}" matBadgeOverlap="true" matBadgeSize="medium" matBadgeColor="accent" color="primary" routerLink="/history"> <span>History</span> </button> </div> </div>

L'extrait de code HTML ci-dessus montre comment utiliser l'élément matBadge de la bibliothèque de matériaux. Pour pouvoir l'utiliser ici, nous devons suivre l'exercice habituel consistant à ajouter MatBadgeModule au fichier app.module.ts . Les badges sont de petits descripteurs d'état picturaux pour les éléments de l'interface utilisateur tels que les boutons, les icônes ou les textes. Dans ce cas, il est utilisé avec un bouton pour afficher le nombre de QR enregistrés par l'utilisateur. Le badge de la bibliothèque de matériaux angulaires a diverses autres propriétés telles que la définition de la position du badge avec matBadgePosition , matBadgeSize pour spécifier la taille et matBadgeColor pour définir la couleur du badge.

Un autre élément d'image doit être ajouté au dossier des éléments : Télécharger. Enregistrez-le dans le dossier /assets du projet.

Ouvrez profile.component.css et ajoutez ceci :

 .center { top: 50%; left: 50%; position: absolute; transform: translate(-50%, -50%); } .profile-child { display: flex; flex-direction: column; align-items: center; } .profile-actions { padding-top: 20px; } .avatar { border-radius: 50%; width: 180px; height: 180px; }

Le CSS ci-dessus réalisera l'interface utilisateur comme prévu.

Pour continuer, nous avons besoin d'une sorte de logique pour mettre à jour la valeur du décompte d'historique, car elle se reflétera dans le matBadge utilisé précédemment. Ouvrez profile.component.ts et ajoutez l'extrait de code suivant de manière appropriée :

 export class ProfileComponent implements OnInit { historyCount = 0; constructor(private storageUtilService: StorageutilService) { } ngOnInit() { this.updateHistoryCount(); } updateHistoryCount() { this.historyCount = this.storageUtilService.getHistoryCount(); } }

Nous avons ajouté StorageutilService mais nous n'avons pas créé un tel service jusqu'à présent. En ignorant l'erreur, nous avons terminé notre composant de profil qui termine également notre composant de page d'accueil. Nous reviendrons sur ce composant de profil après avoir créé notre service utilitaire de stockage. D'accord, alors faisons-le.

Stockage local

HTML5 fournit une fonctionnalité de stockage Web qui peut être utilisée pour stocker des données localement. Cela fournit beaucoup plus de stockage par rapport aux cookies - au moins 5 Mo contre 4 Ko. Il existe deux types de stockage Web avec une portée et une durée de vie différentes : Local et Session . Le premier peut stocker des données de manière permanente tandis que le second est temporaire et pour une seule session. La décision de sélectionner le type peut être basée sur le cas d'utilisation, dans notre scénario, nous voulons enregistrer à travers les sessions, nous allons donc opter pour le stockage local .

Chaque donnée est stockée dans une paire clé/valeur. Nous utiliserons le texte pour lequel le QR est généré comme clé et l'image QR encodée sous forme de chaîne base64 comme valeur. Créez un dossier d'entité, à l'intérieur du dossier, créez un nouveau fichier qr-object.ts et ajoutez l'extrait de code comme indiqué :

Modèle d'entité QR ( Grand aperçu )

Le contenu de la classe :

 export class QR { text: string; imageBase64: string; constructor(text: string, imageBase64: string) { this.imageBase64 = imageBase64; this.text = text; } }

Chaque fois que l'utilisateur enregistre le QR généré, nous allons créer un objet de la classe ci-dessus et enregistrer cet objet à l'aide du service utilitaire de stockage.

Créez un nouveau dossier de services, nous allons créer de nombreux services, il vaut mieux les regrouper.

Dossier Services ( Grand aperçu )

Changez le répertoire de travail actuel en services, cd services , pour créer un nouveau service en utilisant ng gs <any name> . Ceci est un raccourci pour ng generate service <any name> , tapez ng gs storageutil et appuyez sur Entrée

Cela créera deux fichiers :

  • storageutil.service.ts
  • storageutil.service.spec.ts

Ce dernier sert à écrire des tests unitaires. Ouvrez storageutil.service.ts et ajoutez ceci :

 private historyCount: number; constructor() { } saveHistory(key : string, item :string) { localStorage.setItem(key, item) this.historyCount = this.historyCount + 1; } readHistory(key : string) : string { return localStorage.getItem(key) } readAllHistory() : Array<QR> { const qrList = new Array<QR>(); for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); const value = localStorage.getItem(key); if (key && value) { const qr = new QR(key, value); qrList.push(qr); } } this.historyCount = qrList.length; return qrList; } getHistoryCount(): number { if (this.historyCount) { return this.historyCount; } this.readAllHistory(); return this.historyCount; } deleteHistory(key : string) { localStorage.removeItem(key) this.historyCount = this.historyCount - 1; }

Importez la classe qr-object pour corriger les éventuelles erreurs. Pour utiliser la fonction de stockage local, il n'est pas nécessaire d'importer quoi que ce soit de nouveau, utilisez simplement le mot-clé localStorage pour enregistrer ou obtenir une valeur basée sur une clé.

Maintenant, ouvrez à nouveau le fichier profile.component.ts et importez la classe StorageutilService pour terminer correctement le composant de profil.

En exécutant le projet, nous pouvons voir que la page d'accueil est en place comme prévu.

Ajout de Créer une page QR

Nous avons notre page d'accueil prête, bien que le bouton créer/ajouter ne fasse rien. Ne vous inquiétez pas, la logique réelle a déjà été écrite. Nous avons utilisé une directive routerLink pour changer le chemin de base de l'URL en /create mais aucun mappage n'a été ajouté au fichier app-routing.module.ts .

Créons un composant qui traitera de la création de nouveaux codes QR, tapez ng gc create-qr et appuyez sur entrée pour générer un nouveau composant.

Ouvrez le fichier app-routing.module.ts et ajoutez l'entrée ci-dessous au tableau routes :

 { path: 'create', component: CreateQrComponent },

Cela mappera le CreateQRComponent avec l'URL /create .

Ouvrez create-qr.components.html et remplacez le contenu par ceci :

 <app-header [showBackButton]="showBackButton" [currentTitle]="title" [showHistoryNav]="showHistoryNav"></app-header> <mat-card class="qrCard" [class.mat-elevation-z12]=true> <div class="qrContent"> <!--Close button section--> <div class="closeBtn"> <button mat-icon-button color="accent" routerLink="/" matTooltip="Close"> <mat-icon> <i class="material-icons md-48">close</i> </mat-icon> </button> </div> <!--QR code image section--> <div class="qrImgDiv"> <img *ngIf="!showProgressSpinner" src={{qrCodeImage}} width="200px" height="200px"> <mat-spinner *ngIf="showProgressSpinner"></mat-spinner> <div class="actionButtons" *ngIf="!showProgressSpinner"> <button mat-icon-button color="accent" matTooltip="Share this QR"> <mat-icon> <i class="material-icons md-48">share</i> </mat-icon> </button> <button mat-icon-button color="accent" (click)="saveQR()" matTooltip="Save this QR"> <mat-icon> <i class="material-icons md-48">save</i> </mat-icon> </button> </div> </div> <!--Textarea to write any text or link--> <div class="qrTextAreaDiv"> <mat-form-field> <textarea matInput [(ngModel)]="qrText" cdkTextareaAutosize cdkAutosizeMinRows="4" cdkAutosizeMaxRows="4" placeholder="Enter a website link or any text..."></textarea> </mat-form-field> </div> <!--Create Button--> <div class="createBtnDiv"> <button class="createBtn" mat-raised-button color="accent" matTooltip="Create new QR code" matTooltipPosition="above" (click)="createQrCode()">Create</button> </div> </div> </mat-card>

L'extrait ci-dessus utilise de nombreux éléments de la bibliothèque de matériaux angulaires. Comme prévu, il a une référence de composant d'en-tête dans laquelle les paramètres requis sont passés. La prochaine étape est le corps principal de la page de création ; il se compose d'une carte de matériau angulaire ou d'une carte de mat-card centrée et élevée jusqu'à 12px lorsque [class.mat-elevation-z12]=true est utilisé.

La carte de matériau est juste un autre type de conteneur qui peut être utilisé comme n'importe quelle autre balise div . Bien que la bibliothèque de matériaux fournisse certaines propriétés pour disposer des informations bien définies dans une mat-card telles que le placement de l'image, le titre, le sous-titre, la description et l'action, comme on peut le voir ci-dessous.

Exemple de carte ( Grand aperçu )

Dans l'extrait de code HTML ci-dessus, nous avons utilisé mat-card comme n'importe quel autre conteneur. Un autre élément de la bibliothèque de matériaux utilisé est matTooltip ; il s'agit simplement d'une autre info-bulle facile à utiliser, affichée lorsque l'utilisateur survole ou appuie longuement sur un élément. Utilisez simplement l'extrait ci-dessous pour afficher l'info-bulle :

 matTooltip="Any text you want to show"

Il peut être utilisé avec des boutons d'icônes ou tout autre élément de l'interface utilisateur pour transmettre des informations supplémentaires. Dans le contexte de l'application, il affiche des informations sur le bouton de l'icône de fermeture. Pour modifier le placement de l'info-bulle, matTooltipPosition est utilisé :

 matTooltip="Any text you want to show" matTooltipPosition="above"

Outre matTooltip , mat-spinner est utilisé pour afficher la progression du chargement. Lorsque l'utilisateur clique sur le bouton "Créer", un appel réseau est effectué. C'est à ce moment que la roulette de progression s'affiche. Lorsque l'appel réseau revient avec le résultat, nous cachons simplement le spinner. Il peut être utilisé simplement comme ceci :

 <mat-spinner *ngIf="showProgressSpinner"></mat-spinner>

showProgressSpinner est une variable booléenne utilisée pour afficher/masquer le spinner de progression. La bibliothèque fournit également d'autres paramètres comme [color]='accent' pour changer la couleur, [mode]='indeterminate' pour changer le type de spinner de progression. Un spinner de progression indéterminé ne montrera pas la progression de la tâche tandis qu'un spinner déterminé peut avoir des valeurs différentes pour refléter la progression de la tâche. Ici, un spinner indéterminé est utilisé car nous ne savons pas combien de temps prendra l'appel réseau.

La bibliothèque de matériaux fournit une variante de textarea conforme à la directive sur les matériaux, mais elle ne peut être utilisée que comme descendant de mat-form-field . L'utilisation de material textarea est aussi simple que celle de HTML par défaut, comme ci-dessous :

 <mat-form-field> <textarea matInput placeholder="Hint text"></textarea> </mat-form-field>

matInput est une directive qui permet à la balise d' input native de fonctionner avec mat-form-field . La propriété d' placeholder permet d'ajouter n'importe quel texte d'astuce pour l'utilisateur.

ASTUCE : Utilisez la propriété textarea cdkTextareaAutosize pour la rendre auto-redimensionnable. Utilisez cdkAutosizeMinRows et cdkAutosizeMaxRows pour définir les lignes et les colonnes et les trois ensemble pour que la zone de texte se redimensionne automatiquement jusqu'à ce qu'elle atteigne la limite maximale de lignes et de colonnes définie.

Pour utiliser tous ces éléments de la bibliothèque de matériaux, nous devons les ajouter dans le fichier app.module.ts .

Création d'importations de modules QR ( Grand aperçu )

Une image d'espace réservé est utilisée dans le HTML. Téléchargez-le et enregistrez-le dans le dossier /assets .

Le code HTML ci-dessus nécessite également un style CSS. Ouvrez donc le fichier create-qr.component.ts et ajoutez ce qui suit :

 .qrCard { display: flex; flex-direction: column; align-items: center; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 20%; height: 65%; padding: 50px 20px; } .qrContent { display: flex; flex-direction: column; align-items: center; width: 100%; } .qrTextAreaDiv { width: 100%; display: flex; flex-direction: row; justify-content: center; padding: 0px 0px; position: absolute; bottom: 10%; } .createBtn { left: 50%; transform: translate(-50%, 0px); width: 80%; } .createBtnDiv { position: absolute; bottom: 5%; width: 100%; } .closeBtn { display: flex; flex-direction: row-reverse; align-items: flex-end; width: 100%; margin-bottom: 20px; } .closeBtnFont { font-size: 32px; color: rgba(0,0,0,0.75); } .qrImgDiv { top: 20%; position: absolute; display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; } .actionButtons { display: flex; flex-direction: row; padding-top: 20px; }

Connectons l'interface utilisateur avec la logique. Ouvrez le fichier create-qr.component.ts et ajoutez le code ci-dessous, en laissant les lignes déjà présentes :

 export class CreateQrComponent implements OnInit { qrCodeImage = '../../../assets/download.png'; showProgressSpinner = false; qrText: string; currentQR; showBackButton = true; title = 'Generate New QR Code'; showHistoryNav = true; constructor(private snackBar: MatSnackBar, private restutil: RestutilService, private storageService: StorageutilService) { } ngOnInit() { } createQrCode() { //Check if any value is given for the qr code text if (!!this.qrText) { //Make the http call to load qr code this.loadQRCodeImage(this.qrText); } else { //Show snackbar this.showSnackbar('Enter some text first') } } public loadQRCodeImage(text: string) { // Show progress spinner as the request is being made this.showProgressSpinner = true; // Trigger the API call this.restutil.getQRCode(text).subscribe(image =>{ // Received the result - as an image blob - require parsing this.createImageBlob(image); }, error => { console.log('Cannot fetch QR code from the url', error) // Hide the spinner - show a proper error message this.showProgressSpinner = false; }); } private createImageBlob(image: Blob) { // Create a file reader to read the image blob const reader = new FileReader(); // Add event listener for "load" - invoked once the blob reading is complete reader.addEventListener('load', () => { this.qrCodeImage = reader.result.toString(); //Hide the progress spinner this.showProgressSpinner = false; this.currentQR = reader.result.toString(); }, false); // Read image blob if it is not null or undefined if (image) { reader.readAsDataURL(image); } } saveQR() { if (!!this.qrText) { this.storageService.saveHistory(this.qrText, this.currentQR); this.showSnackbar('QR saved') } else { //Show snackbar this.showSnackbar('Enter some text first') } } showSnackbar(msg: string) { //Show snackbar this.snackBar.open(msg, '', { duration: 2000, }); } }

Pour fournir aux utilisateurs des informations contextuelles, nous utilisons également MatSnackBar de la bibliothèque de conception de matériaux. Cela apparaît comme une fenêtre contextuelle sous l'écran et reste pendant quelques secondes avant de disparaître. Ce n'est pas un élément mais plutôt un service qui peut être invoqué à partir du code Typescript.

L'extrait ci-dessus avec le nom de méthode showSnackbar montre comment ouvrir un snack-bar, mais avant de pouvoir l'utiliser, nous devons ajouter l'entrée MatSnackBar dans le fichier app.module.ts comme nous l'avons fait pour d'autres éléments de la bibliothèque de matériaux.

ASTUCE : dans les versions récentes de la bibliothèque de matériaux angulaires, il n'existe aucun moyen simple de modifier le style du snackbar. Au lieu de cela, il faut faire deux ajouts au code.

Tout d'abord, utilisez le CSS ci-dessous pour modifier les couleurs d'arrière-plan et de premier plan :

 ::ng-deep snack-bar-container.snackbarColor { background-color: rgba(63, 81, 181, 1); } ::ng-deep .snackbarColor .mat-simple-snackbar { color: white; }

Deuxièmement, utilisez une propriété appelée panelClass pour définir le style sur la classe CSS ci-dessus :

 this.snackBar.open(msg, '', { duration: 2000, panelClass: ['snackbarColor'] });

Les deux combinaisons ci-dessus permettront de personnaliser le style du composant snackbar de la bibliothèque de conception de matériaux.

Ceci termine les étapes de création d'une page QR, mais il manque encore une pièce. En vérifiant le fichier create-qr.component.ts , il affichera une erreur concernant la pièce manquante. La pièce manquante à ce puzzle est RestutilService qui est responsable de la récupération de l'image du code QR à partir de l'API tierce.

Dans le terminal, changez le répertoire actuel en services en tapant ng gs restutil et en appuyant sur Entrée. Cela créera les fichiers RestUtilService. Ouvrez le fichier restutil.service.ts et ajoutez cet extrait :

 private edgeSize = '300'; private BASE_URL = 'https://api.qrserver.com/v1/create-qr-code/?data={data}!&size={edge}x{edge}'; constructor(private httpClient: HttpClient) { } public getQRCode(text: string): Observable { // Create the url with the provided data and other options let url = this.BASE_URL; url = url.replace("{data}", text).replace(/{edge}/g, this.edgeSize); // Make the http api call to the url return this.httpClient.get(url, { responseType: 'blob' }); } private edgeSize = '300'; private BASE_URL = 'https://api.qrserver.com/v1/create-qr-code/?data={data}!&size={edge}x{edge}'; constructor(private httpClient: HttpClient) { } public getQRCode(text: string): Observable { // Create the url with the provided data and other options let url = this.BASE_URL; url = url.replace("{data}", text).replace(/{edge}/g, this.edgeSize); // Make the http api call to the url return this.httpClient.get(url, { responseType: 'blob' }); }

Le service ci-dessus récupère l'image QR à partir de l'API tierce et puisque la réponse n'est pas de type JSON, mais une image, nous spécifions donc le responseType comme 'blob' dans l'extrait ci-dessus.

Angular fournit la classe HttpClient pour communiquer avec n'importe quel serveur prenant en charge HTTP. Il fournit de nombreuses fonctionnalités telles que le filtrage de la demande avant qu'elle ne soit déclenchée, la récupération de la réponse, l'activation du traitement de la réponse via des rappels et autres. Pour utiliser la même chose, ajoutez une entrée pour le HttpClientModule dans le fichier app.module.ts .

Enfin, importez ce service dans le fichier create-qr.component.ts pour terminer la création du code QR.

Mais attendez! Il y a un problème avec la logique QR de création ci-dessus. Si l'utilisateur utilise encore et encore le même texte pour générer le QR, cela entraînera un appel réseau. Une façon de remédier à cela consiste à mettre en cache la demande, servant ainsi la réponse du cache si le texte de la demande est le même.

Demande de mise en cache

Angular fournit un moyen simplifié d'effectuer des appels HTTP, HttpClient, ainsi que des HttpInterceptors pour inspecter et transformer les requêtes HTTP ou les réponses vers et depuis les serveurs. Il peut être utilisé pour l'authentification ou la mise en cache et bien d'autres choses de ce genre, plusieurs intercepteurs peuvent être ajoutés et chaînés pour un traitement ultérieur. Dans ce cas, nous interceptons les requêtes et servons la réponse du cache si le texte QR est le même.

Créez un dossier d'intercepteur, puis créez un fichier cache-interceptor.ts :

Intercepteur de cache ( Grand aperçu )

Ajoutez l'extrait de code ci-dessous au fichier :

 import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpResponse, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http'; import { tap } from 'rxjs/operators'; import { of, Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class RequestCachingService implements HttpInterceptor { private cacheMap = new Map<string, HttpResponse<any>>(); constructor() { } intercept(req: HttpRequest , next: HttpHandler): Observable<HttpEvent<any>> { const cachedResponse = this.cacheMap.get(req.urlWithParams); if (cachedResponse) { return of(cachedResponse); } return next.handle(req).pipe(tap(event => { if (event instanceof HttpResponse) { this.cacheMap.set(req.urlWithParams, event); } })) } } import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpResponse, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http'; import { tap } from 'rxjs/operators'; import { of, Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class RequestCachingService implements HttpInterceptor { private cacheMap = new Map<string, HttpResponse<any>>(); constructor() { } intercept(req: HttpRequest , next: HttpHandler): Observable<HttpEvent<any>> { const cachedResponse = this.cacheMap.get(req.urlWithParams); if (cachedResponse) { return of(cachedResponse); } return next.handle(req).pipe(tap(event => { if (event instanceof HttpResponse) { this.cacheMap.set(req.urlWithParams, event); } })) } }

Dans l'extrait de code ci-dessus, nous avons une carte avec la clé étant l'URL de la demande et la réponse en tant que valeur. Nous vérifions si l'URL actuelle est présente dans la carte ; if it is, then return the response (the rest is handled automatically). If the URL is not in the map, we add it.

We are not done yet. An entry to the app.module.ts is required for its proper functioning. Add the below snippet:

 import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { CacheInterceptor } from './interceptor/cache-interceptor'; providers: [ { provide: HTTP_INTERCEPTORS, useClass: CacheInterceptor, multi: true } ],

This adds the caching feature to our application. Let's move on to the third page, the History page.

Adding The History Page

All the saved QR codes will be visible here. To create another component, open terminal type ng gc history and press Enter.

Open history.component.css and add the below code:

 .main-content { padding: 5% 10%; } .truncate { width: 90%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .center-img { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: flex; flex-direction: column; align-items: center; }

Open history.component.html and replace the content with this:

 <app-header [showBackButton]="showBackButton" [currentTitle]="title" [showHistoryNav]="showHistoryNav"></app-header> <div class="main-content"> <mat-grid-list cols="4" rowHeight="500px" *ngIf="historyList.length > 0"> <mat-grid-tile *ngFor="let qr of historyList"> <mat-card> <img mat-card-image src="{{qr.imageBase64}}"> <mat-card-content> <div class="truncate"> {{qr.text}} </div> </mat-card-content> <mat-card-actions> <button mat-button (click)="share(qr.text)">SHARE</button> <button mat-button color="accent" (click)="delete(qr.text)">DELETE</button> </mat-card-actions> </mat-card> </mat-grid-tile> </mat-grid-list> <div class="center-img" *ngIf="historyList.length == 0"> <img src="../../assets/no-see.png" width="256" height="256"> <span>Nothing to see here</span> </div> </div>

As usual, we have the header component at the top. Then, the rest of the body is a grid list that will show all the saved QR codes as individual mat-card . For the grid view, we are using mat-grid-list from the Angular material library. As per the drill, before we can use it, we have to first add it to the app.module.ts file.

La liste de grille de tapis agit comme un conteneur avec plusieurs enfants de tuiles appelés mat-grid-tile . Dans l'extrait de code HTML ci-dessus, chaque mosaïque est créée à l'aide mat-card en utilisant certaines de ses propriétés pour le placement générique d'autres éléments de l'interface utilisateur. Nous pouvons fournir le number of columns et rowHeight , qui est utilisé pour calculer automatiquement la largeur. Dans l'extrait ci-dessus, nous fournissons à la fois le nombre de colonnes et la valeur rowHeight .

Nous utilisons une image d'espace réservé lorsque l'historique est vide, téléchargez-la et ajoutez-la au dossier des actifs.

Pour implémenter la logique de remplissage de toutes ces informations, ouvrez le fichier history.component.ts et ajoutez l'extrait ci-dessous dans la classe HistoryComponent :

 showBackButton = true; title = 'History'; showHistoryNav = false; historyList; constructor(private storageService: StorageutilService, private snackbar: MatSnackBar ) { } ngOnInit() { this.populateHistory(); } private populateHistory() { this.historyList = this.storageService.readAllHistory(); } delete(text: string) { this.storageService.deleteHistory(text); this.populateHistory(); } share(text: string) { this.snackbar.open(text, '', {duration: 2000,}) }

La logique ci-dessus récupère simplement tous les QR enregistrés et remplit la page avec. Les utilisateurs peuvent supprimer le QR enregistré, ce qui supprimera l'entrée du stockage local.

Donc, cela termine notre composant d'histoire... ou le fait-il ? Nous devons encore ajouter le mappage de route pour ce composant. Ouvrez app-routing.module.ts et ajoutez également un mappage pour la page d'historique :

 { path: 'history', component: HistoryComponent },

L'ensemble du tableau de routes devrait maintenant ressembler à ceci :

 const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'create', component: CreateQrComponent }, { path: 'history', component: HistoryComponent }, ];

C'est maintenant le bon moment pour exécuter l'application pour vérifier le flux complet, alors ouvrez le terminal et tapez ng serve et appuyez sur Entrée. Ensuite, rendez-vous sur localhost:4200 pour vérifier le fonctionnement de l'application.

Ajouter à GitHub

Avant de passer à l'étape de déploiement, il serait bon d'ajouter le projet à un référentiel GitHub.

  1. Ouvrez GitHub.
  2. Créez un nouveau référentiel.
  3. Nouveau référentiel GitHub ( Grand aperçu )
  4. Dans VS Code, utilisez le terminal et suivez le premier ensemble de commandes mentionnées dans le guide de démarrage rapide pour pousser tous les fichiers du projet.
  5. Ajouter un projet dans GitHub ( Grand aperçu )

Actualisez simplement la page pour vérifier si tous les fichiers sont visibles. À partir de ce moment, toutes les modifications de git (telles que commit, pull/push) seront reflétées dans ce référentiel nouvellement créé.

Netlification et déploiement

Notre application s'exécute sur notre machine locale, mais pour permettre à d'autres d'y accéder, nous devons la déployer sur une plate-forme cloud et l'enregistrer sur un nom de domaine. C'est là que Netlify entre en jeu. Il fournit des services de déploiement continu, une intégration avec GitHub et de nombreuses autres fonctionnalités dont vous pouvez bénéficier. À l'heure actuelle, nous souhaitons permettre un accès mondial à notre application. Commençons.

  1. Inscrivez-vous sur Netlify.
  2. Depuis le tableau de bord, cliquez sur le bouton Nouveau site depuis Git .
  3. Netlify nouveau site ( Grand aperçu )
  4. Cliquez sur GitHub dans l'écran suivant.
  5. Netlify select fournisseur git ( Grand aperçu )
  6. Autorisez Netlify à pouvoir accéder à vos référentiels GitHub.
  7. Autorisation Netlify GitHub ( Grand aperçu )
  8. Recherchez et sélectionnez le référentiel qr nouvellement créé.
  9. Sélection du référentiel Netlify GitHub ( Grand aperçu )
  10. Netlify, à l'étape suivante, nous permet de choisir la branche de référentiel GitHub pour les déploiements. Normalement, on utilise la branche master , mais on peut aussi avoir une branche de release distincte qui ne contient que des fonctionnalités liées à la version et stables.
  11. Construire et déployer Netlify ( Grand aperçu )

Puisqu'il s'agit d'une application Web angulaire, ajoutez ng build --prod comme commande de construction. Les répertoires publiés seront dist/qr comme mentionné dans le fichier angular.json .

Chemin de construction angulaire ( Grand aperçu )

Cliquez maintenant sur le bouton Deploy site qui déclenchera une génération de projet avec la commande ng build --prod et affichera le fichier sur dist/qr .

Puisque nous avons fourni les informations de chemin à Netlify, il récupérera automatiquement les fichiers corrects pour la maintenance de l'application Web. Netlify ajoute un domaine aléatoire à notre application par défaut.

Site Netlify déployé ( Grand aperçu )

Vous pouvez maintenant cliquer sur le lien fourni dans la page ci-dessus afin d'accéder à l'application de n'importe où. Enfin, l'application a été déployée.

Domaine personnalisé

Dans l'image ci-dessus, l'URL de notre application est affichée tandis que le sous-domaine est généré de manière aléatoire. Changeons cela.

Cliquez sur le bouton Domain settings puis dans la section Domaines personnalisés cliquez sur le menu à 3 points et sélectionnez Edit site name .

Domaine personnalisé ( Grand aperçu )

Cela ouvrira une fenêtre contextuelle dans laquelle un nouveau nom de site peut être saisi ; ce nom doit être unique sur le domaine Netlify. Entrez n'importe quel nom de site disponible et cliquez sur Enregistrer .

Nom du site ( Grand aperçu )

Maintenant, le lien vers notre application sera mis à jour avec le nouveau nom du site.

Tests fractionnés

Une autre fonctionnalité intéressante offerte par Netlify est le test fractionné. Il permet la répartition du trafic afin que différents ensembles d'utilisateurs interagissent avec différents déploiements d'applications. Nous pouvons ajouter de nouvelles fonctionnalités à une branche différente et diviser le trafic vers le déploiement de cette branche, analyser le trafic, puis fusionner la branche de fonctionnalité avec la branche de déploiement principale. Configurons-le.

La condition préalable à l'activation des tests fractionnés est un référentiel GitHub avec au moins deux branches. Accédez au référentiel d'applications dans GitHub qui a été créé précédemment et créez une nouvelle branche a .

Créer une nouvelle branche ( Grand aperçu )

Le référentiel aura désormais une branche master et a branche. Netlify doit être configuré pour effectuer des déploiements de succursales, alors ouvrez le tableau de bord Netlify et cliquez sur Settings . A gauche, cliquez sur Build & Deploy , puis Continuous Deployment , puis à droite dans la section Deploy contexts , cliquez sur Edit settings .

Déploiements de succursales ( Grand aperçu )

Dans la sous-section Branch deploys de branche, sélectionnez l'option "Laissez-moi ajouter des branches individuelles", entrez les noms des branches et enregistrez-les.

Le déploiement de braces est une autre fonctionnalité utile fournie par Netlify ; nous pouvons sélectionner les branches du référentiel GitHub à déployer, et nous pouvons également activer les aperçus pour chaque demande d'extraction vers la branche master avant la fusion. Il s'agit d'une fonctionnalité intéressante permettant aux développeurs de tester leurs modifications en direct avant d'ajouter leurs modifications de code à la branche de déploiement principale.

Maintenant, cliquez sur l'option de l'onglet Split Testing en haut de la page. Les configurations de test fractionné seront présentées ici.

Tests fractionnés ( Grand aperçu )

Nous pouvons sélectionner la branche (autre que la branche de production) — dans ce cas a . Nous pouvons également jouer avec les paramètres de répartition du trafic. En fonction du pourcentage de trafic alloué à chaque branche, Netlify redirigera certains utilisateurs vers l'application déployée à l'aide de la branche a et d'autres vers la branche master . Après la configuration, cliquez sur le bouton Start test pour activer la répartition du trafic.

ASTUCE : Netlify peut ne pas reconnaître que le référentiel GitHub connecté a plus d'une branche et peut donner cette erreur :

Erreur de test fractionné ( Grand aperçu )

Pour résoudre ce problème, reconnectez-vous simplement au référentiel à partir des options de Build & Deploy .

Netlify fournit également de nombreuses autres fonctionnalités. Nous venons de passer en revue certaines de ses fonctionnalités utiles pour démontrer la facilité de configuration de différents aspects de Netlify.

Cela nous amène à la fin de notre voyage. Nous avons créé avec succès une conception de matériau angulaire basée sur une application Web et l'avons déployée sur Netlify.

Conclusion

Angular est un cadre formidable et populaire pour le développement d'applications Web. Avec la bibliothèque officielle de conception de matériaux angulaires, il est beaucoup plus facile de créer des applications qui respectent les spécifications de conception de matériaux pour une interaction très naturelle avec les utilisateurs. De plus, l'application développée avec un excellent framework doit utiliser une excellente plate-forme de déploiement, et Netlify n'est que cela. Avec une évolution constante, un excellent support et une pléthore de fonctionnalités, c'est sûrement une excellente plate-forme pour apporter des applications Web ou des sites statiques aux masses. Espérons que cet article vous aidera à démarrer avec un nouveau projet Angular, de la simple réflexion au déploiement.

Lectures complémentaires

  • Architecture angulaire
  • Composants matériels plus angulaires
  • En savoir plus sur les fonctionnalités de Netlify