Pour aujourd’hui je vous propose une petite implémentation d’un thème sous React avec la lib Material-Ui, l’implémentation de Material Design de Google.
Je vous met mon dépot Github pour avoir le code source complet du projet.
Objectifs
Thème dark & light dans toute l’app
Système de sauvegarde des préférences utilisateurs via local-storage
Pré-requis
Il va vouloir quelques dépendances NPM tel que:
React
Material-ui
Initialisation de CssBaseline
On ajouter le composant CssBaseline en entrée de notre application. Mais à quoi set t-il ?
Il va nous permettre de remplacer certaines props CSS de base de notre app, faire certains reset. Cela va permettre d’éviter certains props de mal switcher entre Dark et Light theme.
Nous avons donc dans notre fichier root :
ThemeHandler: on va décrire ce composant juste ensuite 😉
CssBaseline: notre composant pour le bon switch dark/light
ContentApp: notre composant qui va contenir le reste de notre app
Disons qu’il fait un semblant de normalize.css, mais pour Mui 🙂
Création de la palette de notre thème
On va définir dans ce composant tout les hyperparamètre qui touche à l’aspect visuel du thème, ainsi que sa palette de couleurs.
On va définir les props suivants:
local_storage_key: string/id de la props qui nous permet de savoir le thème courant de l’utilisateur. On va détailler le local storage plus tard dans l’article
baseTheme: thème parent qui va définir les paramètres globaux qui seront utilisé à la fois pour le dark que le light
dark_theme: palette spécifique au thème dark
light_theme: palette spécifique au thème light
Création du contexte du thème
On va définir un contexte pour notre thème. Cela va nous permettre au sein de l’ensemble de notre application de récupérer des informations à propos du thème. Il va nous être important dans la suite de l’article. Pour le moment on initialise un contexte vide :
Création du gestionnaire de thème
On souhaite maintenant un système de gestion de thème. On souhaite qu’il garde l’état actuel du thème (palette dark ou light), mais aussi qu’il ait une fonction permettant de switch de thème.
Nous définissions en premier lieu un useState. Il va nous permettre de changer la valeur du thème en cours, mais aussi de la récupérer. On va l’initialiser avec le local storage de l’user. Il va nous permettre de garder la préférence de l’user dans le navigateur ( une sorte de cookie )
On init un objet de contexte avec :
isDark: boolean si le thème en cours est dark
toggleTheme: function permettant le switch de thème
La fonction toggleTheme permet de setter une nouvelle valeur du boolean, et permet de mettre à jour la valeur du local storage.
La fonction getTheme permet de retourner la bonne palette de couleurs en fonction du boolean courant.
D’un point de vue HTML, le composant ThemeHandler est composé de la sorte:
themeContext.Provider: permet de consommer une valeur par défault de context ( on la init auparavant)
themeProvider: permet de fournir un thème à notre application avec celui défini dans le context précédent
{props}: permet d’insérer les nodes enfants que l’on fournira au composant
Ajout de contenu pour notre app
Le système de switch et gestion de thème est mis en place. Il nous faut maintenant quelques composants UI avec un toggle, permettant de switch entre dark et light.
Grace au useContext, on va pouvoir récupérer le context du thème. Ceci va nous permettre de connaître le type de thème actuel au sein de notre application, ainsi que de la function pour switch de thème.
A partir de ce context, vous pouvez ajouter un bouton, un switch ou autre afin de le bind avec la function de switch de theme toggleTheme, et aussi d’initialiser votre composant avec l’état en cours via le isDark.
Nous avons vu dans le chapitre précédent comment stocker en local des données simples.
On va s’attarder cette fois-ci à comment stocker des informations plus nombreuses, plus complexes et ce de façon plus ordonnée, en utilisant des bases de données SQL.
On va réaliser une simple to-do app, avec possibilité d’ajouter une note, ou d’en supprimer, avec une liste permettant de toute les afficher.
On ne peut utiliser n’importe quel outils pour persister des données car nous sommes sur un projet Expo, nous empêchant d’accéder aux modules natifs de iOS et Android. SQLite est un moyen fonctionnant avec des projets Expo.
Objectifs
Stocker localement des informations via une base de donnée SQL
Prérequis
Installez le package pour utiliser SQLite :
expo install expo-sqlite
Nous l’importerons dans nos fichiers de la manière suivante :
import*asSQLitefrom'expo-sqlite';
Base de données & CRUD
CRUD : diminutif correspondant aux requêtes basique, à savoir Create, Read, Update et Delete.
Création de la base de données
Rien de plus simple, une seule ligne suffit :
On donne un nom à notre base, en argument. Celle-ci n’est crée qu’une seule et unique fois. Si on rappelle cette même méthode, on récupère la base de donnée crée auparavant, aucun risque de doublon.
Création de la table NOTE
On va maintenant créer notre table. Je vous montre la façon la plus simple de réaliser des transaction SQL vers notre base :
Requête SQL des plus basique avec le mot clé CREATE TABLE. On lui donne un nom de table, ainsi que la définitions de nos colonnes. Un ID auto-incrémenté pour garantir l’unicité de nos données, ainsi qu’un attribut TEXT qui contiendra le contenu de nos notes.
Nos deux attributs ne peuvent être null, et on leur défini un type, soit Text ou Integer.
Optimiser les requêtes
Je préfère une autre annotation que la précédente pour questionner la base de données, récupérer nos objets et leurs affecter des transformations. Je trouve plus clair et simple à l’utilisation. On définit notre modèle de requête avec une promesse :
On pourra utiliser ce modèle dans des fonctions async, via un appel par un await. On pourra utiliser des then() et catch() à l’appel du service dans nos vues, permettant par exemple d’afficher à l’utilisateur dans une popup si une note à bien été ajouté ou si dans le cas inverse afficher un message d’erreur avec son origine.
Récupérer toute les notes
On définit une interface pour les objets que l’on va récupérer en base. Cela nous facilitera leurs manipulations au sein de notre application :
On fait appel à notre modèle pour questionner la base :
On récupère nos objets via la requête. On va itérer sur notre résultat de requête pour re-typer correctement nos objets.
Ajouter une note
Seule différence, on va ici passer en argument le contenu de ma note que l’on souhaite persister en base. Pas besoin de lui passer un ID pour la note, car celle-ci est généré automatiquement en base. Nous avons défini cette option tout à l’heure, lors de la création de la table.
Supprimer une note
Quasiment identique au point précédent, sauf qu’ici on lui passe un ID de note à notre fonction, étant donné que c’est l’attribut qui défini l’unicité de mes items dans ma table NOTE :
Récupération des items dans la vue
Nous venons de créer notre base ainsi que des opérations CRUD permettant d’interagir avec elle. On va désormais créer une nouvelle vue, avec un champ de texte permettant de donner du contenu à une note, un bouton pour ajouter cette note, une liste scrollable permettant de naviguer sur l’ensemble de nos notes en base, et leur associé à chacun un bouton pour les supprimer.
Définition du state de notre vue
On commence par créer le squelette de base de notre vue :
On utilise ici un composant de classe et non un composant fonctionnel. On a besoin d’utiliser un state pour l’affichage dynamique du contenu de notre liste scrollable. On défini un attribut myNoteList correspondant à une liste de l’ensemble de nos notes en base, ainsi qu’un second attribut note correspondant au champ de texte remplissable par l’utilisateur pour créer une nouvelle note. On initialise les deux attributs dans le constructeur.
Définition des méthodes de notre vue
On défini les méthodes pour mettre à jour nos éléments, pour ajouter une note, ainsi que la supprimer :
La méthode componentDidMount() est une méthode standardisé de React, permettant d’être appelé une seule et unique fois et ce à la fin de la création du composant. On lui demande à l’ouverture de notre page, d’initialiser notre liste avec le contenu de notre base de données.
Définition des éléments visuels de notre vue
Ici rien de bien complexe. J’ai créer un titre de page, un champ de texte avec un bouton pour ajouter une note. Une liste scrollable présentant l’ensemble des notes en base, ainsi qu’une option permettant de les supprimer :
J’ai utilisé des icones Ionicons inclus dans Expo, qui s’adapte en fonction de si vous êtes sur Android ou iOS via la méthode Platform.OS, encapsulé dans des TouchableOpacity, pour rendre un peu plus esthétique mes boutons d’interactions, ainsi que des flexbox pour avoir une touche de responsive design.
Conclusion
Vous venez de voir comment créer très simplement une base de donnée pour votre application React Native tournant sous Expo, comment l’interroger et surtout comment récupérer et afficher le résultats dans une belle vue responsive.
Nous avons des outils dans le développment web, permettant de développer rapidement un squelette de base pour une application :
CLI pour Angular
CRA pour React
CLI pour React Native
Mais pour créer mon premier projet pour découvrir ces technos mobile, je pars non pas sur ReactNative CLI, mais sur ExpoCLI. C’est comme Rails mais pour ReactNative, ou encore Phonegap pour Cordova. Il étends les fonctionnalité de ReactNativeCLI.
Qu’apporte t-il de plus par rapport à React Native CLI et quand l’utiliser ?
Avantage :
Pas besoin d’Android Studio ni de Xcode
Configuration bien plus simple & rapide ( hello world en moins de une minute )
Hot reload, sur smartphone physique et non par simulateur. Oui, via l’appli Expo, vous pouvez tester votre app sur votre smartphone iOS ou Android physique, et ce instantanément. En react native vanilla, vous avez droit au simulateur Android sur Windows, et simulateur iOS sous macOS exclusivement.
Inconvénient :
Poids de l’apk/ipa, du à l’ajout de librairie annexes (~50mo le hello world, ça fait mal…)
Accès aux modules natifs impossible. Vous ne pouvez ajouter de lib natif via Xcode ou Android studio, car tout est packagé via le sdk Expo. Certaines fonctionnalité sont impossible d’être utilisé, comme les achats in-app ou le bluetooth.
En résumé, pour débuter le dév mobile ou pour de simples application, vous devriez commencer par Expo. Pour des utilisateurs expérimentés ou souhaitant des fonctionnalités spécifiques à chaque plateforme, partez sur du ReactNative vanilla.
Prérequis
Il vous faudra de base :
NodeJS
Installez ensuite la CLI de Expo via une console :
npm install -g expo-cli
Pensez à avoir Python dans vos variables d’environnements, au risque d’avoir une erreur d’installation.
Initialisation du squelette
On initialise une nouvelle application via la commande :
expo init "Nom_de_votre_app"
Vous aurez l’architecture suivante :
assets/ : dossier contenant vos images
app.json : fichier de configuration du projet
app.tsx : fichier d’entrée de votre application
package-json : contient l’ensemble des dépendances et scripts du projet
tsconfig.json : fichier de configuration de typescript
babel.config.js : fichier de configuration pour la gen du bundle ( une sorte de webpack )
Lancement du serveur de dév
Simple :
npm start
Un nouvel onglet dans votre navigateur va s’ouvrir, vous proposant divers options :
Pour faire fonctionner le simulateur Android ou iOS, vous allez devoir installer les biblio natives. Mais on peut éviter cela en utilisant notre propre smartphone. On va devoir connecter notre smartphone au même réseau wifi que l’ordinateur sur lequel on développe.
Installez l’application EXPO, disponible sur l’iOS Store et GooglePlay store, selon votre type de device. Une fois l’application lancé sur votre smartphone, vous allez pouvoir scanner le QR code affiché sur la console, comme montré sur la capture d’écran précédent. Cela va permettre de transférer le bundle directement sur le smartphone.
Vous allez pouvoir ainsi voir vos modification en temps réel sur votre téléphone, de l’application que vous codez, à chaque sauvegarde.
Icone & Splash screen
Vous pouvez changer l’icone de votre application via le fichier « app.json ».
De même pour le splash screen, qui correspond a l’image qui sera affiché le temps que votre application soit chargé par votre smartphone.
Conclusion
Nous venons de voir comment réaliser un « HelloWorld » en quelques minutes, et ce directement sur notre smartphone.
On discutera dans le prochain chapitre sur comment générer les fichiers APK et IPA, permettant d’installer et de distribuer notre application.
Nous avons vu au chapitre précédent comment créer notre éditeur de texte, comment communiquer du render au main process via le module IPC afin d’interagir avec les fichiers disques via les API de NodeJS.
Pour ce cours-ci nous allons nous consacrer sur une fonctionnalité apprécié en ce moment, à savoir comment avoir un thème sombre et clair, et comment enregistrer ces données-ci afin de les récupérer à chaque ouverture du logiciel. Une rapide ouverture à la programmation réactive Observer/Observable via la librairie RxJS afin d’implémenter une fonctionnalité pour améliorer l’UX de notre logiciel, afin de renseigner le nom du fichier texte actuellement ouvert par l’utilisateur.
Ce cours sera donc principalement consacré à l’amélioration de l’UI pour l’utilisateur.
Le code source concernant ce chapitre est disponible sur mon Github.
Mise en place du système de persistance
Je vous propose d’ajouter une fonctionnalité, permettant de sauvegarder l’état du thème choisit, pour qu’il puisse être à jour à chaque lancement de l’application et que l’on ne soit pas obligé de le re-sélectionner. D’une manière général, je vous propose un moyen simple afin de sauvegarder et charger des paramètres utilisateurs en cache.
Pour sauvegarder des données, vous pouvez très bien créer une base de données plutôt traditionnel afin de stocker vos informations nécessaire au fonctionnement de l’application. Mais pour notre application qui reste extrêmement minimaliste, je vous propose la sérialisation des data dans un simple fichier JSON. Si vous souhaitez néanmoins rester sur une BD traditionnel, vous devriez regarder vers SQLite database, ou encore IndexedDB sur NPM.
Première chose, on va installer un simple module nous permettant de gagner du temps quand à la persistance de fichier JSON.
npm install electron-store –save
Souvenez vous que nos fichiers seront disponible dans le dossier %Appdata% de votre ordinateur, dans le dossier du nom de votre application.
On commence par vérifier si notre fichier JSON de sauvegarde existe au préalable sur l’ordinateur. Si celui-ci existe on ne fait rien en particulier. Dans le cas contraire, on va initialiser les valeurs des nos couples Clés/Valeurs. On souhaite avoir comme paramètre sauvegardé, le type de thème sélectionné ( Light, Dark ou Custom ) mais aussi les codes couleurs hexadécimal de nos différentes parties de notre application customizable, via le thème ‘Custom’. En effet, autant les thèmes Light et Dark sont pré-défini au sein de l’application et non modifiable, autant le thème Custom est modifiable lui.
On ajoute dans notre main process de Electron les bons imports qui vont bien :
L’argument ‘name‘ représente le nom du fichier JSON de sauvegarde, lors de la création de notre objet ‘storage‘.
On définit ensuite une fonction qui sera appelé dans notre fonction ‘createWindow‘ :
Comme je vous ai dit plus haut, si le fichier ‘settings.json‘ existe déjà, on ne fait rien. Dans le cas inverse, on va le créer et initialiser :
SETTINGS : clé
THEME_TYPE : clé, contient le type de thème à appliquer au lancement de notre app
CUSTO_PALETTE : clé, contient d’autres clés qui sont nos variables CSS avec leur code couleur hexadécimal qui leur sont associé
Thème clair & sombre
Il existe plusieurs façon de créer son propre système de thème pour son application. Nous allons voir une méthode très simple qui utilise les variables CSS. Au lieu d’affecter une couleur en dur à un composant HTML via son fichier CSS ( code en hexadécimal ou RGB ), on va désormais lui affecter une variable. C’est celle si que l’on va aller modifier selon no thème. Cela nous permet ainsi de bind plusieurs couleurs au même composant. Je vous propose de créer un thème clair, foncé, et un custom, auquel on pourra lui affecter au sein même de notre application une couleur modifiable à la volée.
Création des différents modèles de thèmes
Premièrement, on va créer une énumération définissant nos différents type de thème que l’on peut choisir:
Ensuite, nous allons créer une interface permettant de créer des objets du même type :
Dans le même fichier précédent, on va maintenant créer 3 objects correspondant à nos 3 thèmes. Nous allons leur affecter un type ( Light, Dark ou Custom ) et leur définir l’ensembles des propriétés CSS que l’on souhaite modifier. Les propriétés CSS ont été réduite volontairement pour gagner en lisibilité, vous pouvez retrouver l’ensemble des propriétés du projet via son dépôt Github. Si on souhaite modifier par exemple seulement la couleur de fond de notre application :
Le mot clés export nous permet d’y accéder en dehors de ce fichier.
Mise en place de l’interface pour la sélection du thème
Pour la vue de notre page de paramètres, faisons quelque chose de simple. On va disposer d’un groupe de bouton radio, permettant une sélection unique du thème. Cela nous rampement de choisir entre Light, Dark et Custom. Les deux premiers thèmes ayant des couleurs définit dans notre application, seul le Custom pourra être modifiable, histoire de laisser un maximum de liberté à l’utilisateur dans le choix de ses couleurs. Pour la vue, rien de bien complexe :
On ajoute juste une condition, dans le cas ou l’utilisateur choisit le thème ‘Custom’, on affiche un sélecteur de couleur pour chaque item modifiable de notre application. Pensez d’ailleurs à installer ce module que j’apprécie fortement, un color picker simple qui permet de choisir une couleur en héxadecimal, en RGB ou via un arc-en-ciel de couleur, et qui propose une multitude de fonctionnalités via son api, installable via :
npm install material-community-components –save
Petit aperçu du module :
Passons maintenant au contrôleur de notre page de paramètres. Nous devons définir des attributs pour lier nos boutons précédemment expliqués :
availableTheme : Tableau contenant les thèmes disponible à la séléction, responsable de la création des boutons radio
aliasTypeTheme : cet alias est un bind à notre énumération précédente, qui contient les trois type de thèmes. Cela nous permet de pouvoir faire dans la vue notre comparaison, et afficher le color picker si et seulement si l’utilisateur à sélectionné le thème Custom
activeTheme : c’est le thème actuellement activé
Concernant le constructeur du composant, on va créer une instance de ElectronService, permettant de discuter du render process au main process et vice versa :
On initialise nos thèmes disponible pour créer nos boutons radio.
Système pour initialiser un thème au démarrage de l’app via Electron-store
On va déléguer la gestion des préférence utilisateurs à un service, un singleton. Créons le via la CLI de Angular :
ng generate service ThemeManager
On va lui définir un attribut correspond au thème actif, et un second correspondant lui aussi au thème actif, mais qui sera un BehaviorSubject ( le but de celui-ci et de mettre à jour le thème actif au lancement de l’application , lorsque celle-ci chargement au démarrage les préférences de l’utilisateur) :
Concernant le constructeur du service, on va créer une instance de ElectronService permettant de contacter le main process :
On initialise par des valeurs random nos deux précédents attributs (je vous l’accorde niveau QUALITÉ du code, on a fait mieux hein 😅). et on envoi une notification au main process sur le canal ‘loadUserSettings‘.
On va définir dans le main process la méthode permettant de lire dans notre fichier JSON de sauvegarde, les données des préférences (définit par notre clé ‘SETTINGS’, et de l’envoyer au render process via le cannal ‘responseLoadUserSettings‘:
On réceptionne dans le thème manager les données d’initialisation envoyé par le main process :
On actualise notre thème actif, la palette de couleurs pour le thème ‘CUSTOM’, et on met à jour notre BehaviorSubject pour mettre la vue à jour concernant le thème actif, et enfin on appelle une méthode permettant d’appliquer notre thème :
On parcours les variables CSS du thème actuel, et on va les appliquer à notre document. Voilà, notre thème est appliqué au démarrage ! 😄
La dernière étape consiste à mettre à jour le modèle de nos boutons radio suite au chargement du thème initial. On ajoute une fonction dans le constructeur du composant de la page paramètres, afin qu’il soit notifié dès que le thème actif de notre service ThemeManager change. Voilà pourquoi je l’ai définit en tant que Observable, pour lui attacher un observateur. On va pouvoir subscribe a ce Subject de la façon suivante :
Système pour appliquer le thème courant
Maintenant que au lancement de notre application nos préférences sont lues et appliqués, on souhaite pouvoir changer de thème durant l’utilisation de notre application. On retourne sur le contrôleur de notre page de paramètre, et on ajoute la fonction suivante :
Cela va nous permettre de mettre à jour le thème actif pour nos boutons radio mais aussi de l’appliquer, via le ThemeManager.
Système pour sauvegarder le thème courant
Nouvelle fonctionnalités qui peut être cool, c’est de pouvoir enregistrer l’état courant de notre application, concernant le thème actif actuellement. Très simple, on va ajouter une seule ligne à la méthode expliqué juste avant, changeTheme() disponible dans le contrôleur de notre page ‘SETTINGS‘ :
Cela permet à chaque changement de thème, ou de couleur sur le color picker pour le thème CUSTOM ( car cette méthode d’actualisation est aussi appelé lors d’une modification de couleurs ) d’envoyer une notification à notre main process sur le canal ‘saveSettings‘, avec notre thème actuel en data.
Dans le main process, nous ajoutons une méthode permettant de réceptionner le thème actuel :
Et on met à jour notre fichier JSON contenant nos préférences utilisateurs, via Electron-store.
Indication de la page en cours d’exécution
Vous allez avoir besoin d’un nouveau module, permettant de faire de la programmation réactive :
npm install rxjs –save
Histoire d’améliorer l’interface utilisateur, je vous propose d’ajouter au centre de notre footer, une indication pour rappeler à l’utilisateur sur quel page il se situe au sein de notre application. Pour cela, on va définir une énumération contenant l’ensemble des pages de notre application :
Ainsi lorsque l’utilisateur va changer de page via les boutons de navigation situé en haut de notre application, on demandera une mise à jour du footer. Et pour cela, on va avoir besoin d’un service. On va créer ce service depuis la CLI de Angular :
ng generate service FooterUpdateService
Ce service va nous permettre d’échanger des données entre nos différents composants. Il se compose de la façon suivante :
On définit un BehaviorSubject. Ceci est un type de la librairie RxJs, qui propose de réaliser de la programmation réactive. Cela est grosso modo de la programmation qui suit le paradigme Observable et Observer. Ce menuStateSubject est un observable. Le ‘behavior‘ indique que c’est un observable, qui sera initialisé dès sa création avec un état pré-défini. Vous pouvez le voir dans le constructeur du service, on lui définit l’état HOME. Si vous souhaitez de pas lui définir d’état en particulier à sa création, vous pouvez utiliser simplement un Subject à la place du BehaviorSubject.
On a une première méthode, updateMenuStateSubject, permettant de lui envoyer un nouvel état.
La méthode getMenuStateObservable permet de renvoyer notre observable, nous l’utiliserons juste après.
Le but de ces observables, et d’y attacher des observateurs, ou juste de pouvoir subscribe au sujet. Cela permet de notifier à chaque changement d’état de notre Observable, ses observateurs et de pouvoir faire des actions en conséquence.
On va ajouter dans le contrôleur de notre Header, une fonction permettant de mettre à jour l’état de notre Observable, définit dans le service précédent :
N’oubliez pas d’ajouter au constructeur de notre contrôleur Header, l’appel au singleton de notre service via ‘public footerService: FooterServiceUpdate‘. On ajoute un argument à cette fonction, du type de l’énumération précédemment crée, afin de déterminer quelle page on ouvre.
On ajout ensuite le bind (click) dans notre vue sur chacun de nos boutons, qui nous permettent de changer de page. Ils permettent dorénavant de mettre à jour notre observable :
On ajoute dans la vue du footer une variable permettant d’afficher le contenu d’une variable qui sera définit dans son contrôleur :
On va ajouter dans le contrôleur du footer, l’attribut précédemment utilisé pour l’affichage de l’état courant du menu :
Et voilà le résultat !
Conclusion
Nous venons de voir quelques pistes afin d’améliorer l’UI pour notre application, ainsi qu’une piste pour sauvegarder les préférences de l’utilisateur. Nous aurons aussi vu une rapide approche sur la bibliothèque RxJS, permettant de suivre le paradigme Observable/Observer.
Le 6 eme et dernier chapitre de cette formation est consacré au build & package de notre application, afin de pouvoir la distribuer comme n’importe quel logiciel.
Nous avons vu au chapitre précédent comment réaliser un simple appel à NodeJS via le module de communication IPC, afin d’éffectuer un appel au système de fichiers pour nous remonter des fichiers textes disponible dans un dossier précis.
Nous allons maintenant nous attaquer à une nouvelle fonctionnalité, nous permettant lors qu’un clique sur un fichier affiché dans notre explorateur de fichier, d’afficher son contenu dans un nouvel espace.
Le code source concernant ce chapitre est disponible sur mon Github.
Affichage d’un fichier
Notifier le main process
On va reset les classes permettant de surligner le fichier sélectionné, sur l’ensemble de nos fichiers listé dans notre explorateur.
On va ensuite activer seulement le surlignage pour le fichier actuellement sélectionné.
On envoi une notification au main process via le module IPC, sur le canal pingDisplayFile, en lui envoyant en donnée le chemin relatif de ce fichier.
Réponse du main process
On écoute sur ce même canal avec le module IPC.
On va lire le contenu du fichier, via son path précédemment envoyé, via le module fs de Nodejs.
Je récupère aussi le nombre de lignes que contient le fichier, cela nous permettra d’indiquer les lignes dans notre éditeur de texte.
Je créer un objet qui réunit ces deux résultats, afin de l’envoyer au render process via le canal responseFileContent.
Affichage du fichier dans le render process
La première fonction me permet de mettre mon attribut fileContent à jour via le nouveau fichier précédemment envoyé, ainsi que l’attribut lineNumber pour le compteur de nombre de ligne.
La seconde fonction va permettre de sauvegarder notre nouveau contenu affiché, et de l’enregistrer sur notre disque à la place de notre ancien fichier. On envoi une notification via le canal saveFile, en lui donnant en paramètre un object qui contient notre fichier, son contenu ainsi que son chemin d’accès.
On va ensuite construire notre page HTML pour affiche d’une part le contenu de notre fichier d’une façon éditable, mais aussi d’avoir à proximité un compteur de ligne pour se référer.
Cette disposition de flexbox me permet d’avoir deux colonne, via fxLayout= »row » :
lineCounterDisplay : est la partie pour le compteur de ligne. Le trick est de créer une division par chiffre de ligne via le bind angular *ngFor. Pour cela j’ai créer un tableau qui contient n item qui est le nombre de ligne du fichier, et qui sont incrémenté de 1 entre chaque item. Le contenu est bindé via J’ai réaliser via la fonction suivante :
textEditorDisplay : est la partie pour l’édition du fichier texte. Rien de bien spécial, c’est juste un textArea, qui permet d’afficher et de taper du texte. On le bind avec [(ngModel)] pour l’associer à l’attribut de notre contrôleur qui contient les données du fichier texte, envoyé auparavant par notre main process. On utilisera un second bind, (input), qui sera affecté à la fonction précédemment présenté. Celle-ci permet de déclencher la fonction à chaque changement fait dans le textArea. Cela permet de recalculer en temps réel le compteur de ligne à afficher.
Sauvegarde du fichier sur le disque
On utile le module fs de NodeJS pour sauvegarder notre fichier fraîchement modifié.
Conclusion
Vous pouvez rendre plus complexe votre éditeur de fichiers en y ajoutant certaines autres fonctionnalités tel que :
Une sauvegarde automatique via un timer, ou même à chaque ajout/suppression du moindre caractère ?
Proposer des outils afin de customiser la couleurs, taille, ou type de police ?
Direction pour le prochain chapitre qui abordera comment créer des thèmes clair et sombre ( pour reposer les yeux la nuit ), comment gérer le stockage de données utilisateurs, et enfin un rapide tour sur la programmation réactive via le paradigme Observer/Observable via la bibliothèque RxJS !
Nous avons vu au chapitre précédent comment réaliser la barre d’outils principal du logiciel.
Nous allons maintenant rentrer dans le vif du sujet pour la réalisation d’un explorateur de fichier texte basique.
Le code source concernant ce chapitre est disponible sur mon Github.
Explorateur de fichiers redimensionable
C’est tout bête, mais il va arriver à un moment ou à un autre que l’on tombe sur des noms de fichiers plus ou moins long, donc certains pourrait être tronqué. Autant faire quelque chose de propre, et donner la possibilité à notre explorateur d’être redimensionable par l’utilisateur.
Depuis le dernier chapitre, j’ai légèrement modifié l’architecture du projet afin de le rendre plus maintenable pour la suite des tutoriels. En voici une présentation simplifié :
Module App
appComponent.ts
appComponent.html
appComponent.scss
appRouting.ts
Module Settings
settingsComponent.ts
settingsComponent.html
settingsComponent.scss
settingsRouting.ts
Module Home
homeComponent.ts
homeComponent.html
homeComponent.scss
homeRouting.ts
Module Projects
projectsComponent.ts
projectsComponent.html
projectsComponent.scss
projectsRouting.ts
File Explorer
fileExplorerComponent.ts
fileExplorerComponent.html
fileExplorerComponent.scss
File Editor
fileEditorComponent.ts
fileEditorComponent.html
fileEditorComponent.scss
Pour la partie graphique de notre file explorer, il va être composé d’une toolbar pour permettre d’ouvrir un dossier, une partie pour afficher nos fichier en colonne, et enfin un grabber nous permettant de modifier la taille de notre fenêtre :
On va utilise une directive de angular pour nous permettre de bind la largeur de notre file explorer [style.width.px]=’divWidth’.
Pour la partie de style, rien de bien sorcier, juste de prévoir de changer le curseur de souris lorsque l’on passera sur le grabber afin de faire remarquer à l’utilisateur que la fenêtre est modifiable :
La partie la plus intéressante et ou se passe la magie de notre grabber est dans notre contrôleur. On va définir :
un boolean, pour savoir si on a le clique enfoncé ou non,
une largeur de fenêtre,
une ancienne largeur de fenêtre pour connaître les déplacements.
Dernière étape, on va devoir ajouter des listener d’actions :
Un pour savoir quand on bouge la souris,
un autre pour savoir quand on enfonce le clic,
et un dernier pour savoir quand on relâche le clic
Dans l’ordre, cela va nous pouvoir de modifier la taille en temps réel, de savoir quand effectuer cette action, et savoir quand l’arrêter :
Explorateur basique de fichier texte
On souhaite avoir un système qui puisse lister l’ensemble des fichiers textes d’un dossier, et nous l’afficher sous une forme de liste au sein de notre logiciel. Cela nous permettra d’un simple clique de pouvoir ouvrir tel ou tel fichier texte à modifier.
Demande de fichier depuis le Frontend
On va ajouter un bouton nous permettant à son click, de notifier le backend afin d’ouvrir une fenêtre de dialogue pour sélectionner un dossier, dans lequel on souhaite récupérer l’ensemble des fichiers textes qui y sont situé. Rien de bien complexe pour la partie graphique, juste un bouton contenant une icone, qui à son click sera bindé avec l’appel de ‘openFolderDirectory()’. L’utilisation de la flexbox nous permet de positionner le boutton sur le côté gauche de la div.
Dans le contrôleur on va ajouter la fonction précédente. Celle-ci fait appel au module ipcRender, qui permet la communication de message du renderProcess vers le mainProcess. Il permet d’envoyer des messages via la fonction ‘send’, ou d’écouter via ‘on’. Cela fonctionne exactement comme des sockets, si vous en avez déjà utilisé. Dans notre cas on va juste envoyer un message vide, une sorte de ping pour exécuter une fonction.
Traitement de la demande par le Backend
On va réceptionner la notification venant du front via le module ipcMain dans notre backend, avec sa méthode ‘on’. La fonction prend un argument ‘event’, permettant de renvoyer un message, ainsi qu’un second argument ‘message’ contenant des données envoyé. Mais rappelez vous, on a juste ping sans envoyer le donnée, cet argument sera donc vide.
Le module dialog va nous permettre de créer une fenêtre de dialogue, avec en argument notre fenêtre principal, suivit d’un dictionnaire d’options permettant de définir si l’on souhaite ouvrir un dossier, un fichier, de définir un nom de fenêtre en particulier, etc.
Cette fonction nous renvoi une promesse, sur laquelle on va pouvoir lui attache deux bloc :
.then() : est appelé si l’ouverture du dialog se passe correctement,
.catch() : est appelé si une erreur est lancé lors de son ouverture.
Dans le cas ou on a une erreur, on la remonte simplement dans la console du back.
Dans le cas ou tout se passe bien, on va vérifier que l’utilisateur à bien choisit un dossier et n’a tout simplement pas annulé son action par la fermeture de boite de dialogue. On va alors appelé le module fs de Node qui permet de réaliser des opérations de lectures et d’écritures sur le disque. On va dans un premier temps récupérer l’ensemble des fichiers contenu dans le dossier renseigné pour l’utiliser, puis lui appliquer un filtre via une regexp, permettant de garder seulement les fichiers dont leurs noms se termine par .txt.
On va utiliser le premier argument pour pouvoir répondre au front, en lui envoyant dans un nouveau canal, un tableau contenant les noms de fichiers textes étant dans son dossier de sélection.
Affichage de la réponse dans le Frontend
On va réceptionner la réponse venant du Backend via une fonction écoutant sur le même canal que celui utilisé pour l’émission. On va récuperer le tableau de données, et l’associer à un attribut de composant que l’on aura déclaré au préalable.
Maintenant que l’on a nos données, on a plus qu’a les afficher comme une liste dans notre vue. On utilisera une mat list item, et on utilisera le bind *ngFor de Angular pour parcourir l’ensemble des items de notre tableau de données.
Un petit plus pour l’ésthetisme
Vous avez vu le binding [ngClass] ? C’est une directive de Angular, permettant de lui associer une classe CSS en plus (highlight, dans mon cas), si la condition file.highlight est respecté, soit si et seulement si le booléan est à true. Lorsque l’utilisateur clique sur un fichier, une fonction sera appelé. Celle-ci mettra à jour le boolean de l’ensemble des fichiers à faux, et mettra à true sur celui qui a été sélectionné.
Si vous regarder le code, concrètement cela permet lors d’une sélection d’un fichier, de lui ajouter une couleur plus clair que les autres, pour renseigner de façon plus jolie à l’utilisateur, sur quel fichier il est. Cela aura d’avantage de sens lors du prochain chapitre vous verrez 😉
Conclusion
Vous pouvez rendre plus complexe votre explorateur en y ajoutant certaines fonctionnalités comme :
Pouvoir remonter dans le dossier parente : pour cela vous n’avez qu’a juste ajouter un bouton ‘parent’, qui va ré-appeler notre fonction de ping du backend, mais en lui envoyant un chemin avec un niveau plus haut.
Possibilité d’ajouter un logo à côté de chaque fichier, en fonction de leur type. Vous aurez juste besoin d’une fonction qui split le nom d’un fichier, et qui compare l’extension, et affiche un type d’icone en fonction de celle-ci. A faire soit directement dans la vue avec un binding *ngSwitch, soit d’ajouter un nouvel attribut dans notre FileType.
Le prochain chapitre portera sur l’ouverture d’un fichier texte dans notre éditeur, pour pouvoir le modifier et le sauvegarder sur le disque.
Electron est un framework permettant de développer des applications bureaux multi plateforme ( Linux, Windows, MacOS ) avec des technologies web ( HTML, CSS et Typescript/Javascript ). Il est open source et permet de réaliser très rapidement des applications. Vous pensez que cela n’est pas possible ? Et pourtant vous en utilisez surement sans même le savoir ; Atom, Visual studio, Slack pour n’en citer que les plus gros.
Vous allez donc développer votre application comme si vous développiez un site web.
Composition de Electron
Electron embarque plusieurs outils/bibliothèque pour permettre d’avoir les mêmes accès qu’un logiciel développé avec un langage plus adapté et/ou de plus bas niveau :
Chronium : c’est le navigateur open source qui sert de base au célèbre Chrome de Google. Il va assurer le rendu visuel de l’application.
NodeJS : c’est un environnement d’exécution de code javascript. Il permet l’accès au système de fichier de l’ordinateur, ainsi que le réseau.
APIs Natives : permet l’accès aux fonctions natives, propres à chacun des OS.
Fonctionnement global
Le développement en est extrêmement simplifié, mais aussi accéléré, car vous aurez accès à plus de 300 000 modules sur NPM. C’est une sorte d’hébergeur de module, qui permet de réaliser certaines tâches. Vous ajoutez donc en quelques secondes de nouvelles fonctionnalité sur votre application.
D’autant plus que vous pouvez ajouter un framework pour le frontend pour structurer votre application : Angular, React, VueJS…
Vous allez avoir deux processus différent pour faire fonctionner une application tournant sous Electron :
Main process
C’est le point d’entrée de votre application. Il va contrôler le cycle de vie de l’application. Vous aurez tout les accès depuis ce processus, via les API native ou de NodeJS. Il peut aussi créer de nouveau processus de rendu, ainsi que de démarrer et de quitter l’application.
Ce processus est unique
Render process
Il va être responsable de la vue de votre application, par le biais d’affichage de vos pages HTML/CSS. Vous aurez accès au javascript pour gérer les contrôleurs et interactions. Mais attention, pas d’accès direct au système.
Chacun des processus de rendu sont indépendant les uns des autres. Si un crash, il n’affecte pas ses voisins. Il peut être caché, permettant d’exécuter du code en arrière plan.
Ce processus peut être multiple.
/!\ L’ensemble des fonctionnalités disponible par l’API de Electron ne sont pas forcement accessible depuis les deux types processus. Certains ne seront garanti que dans un seul des deux type de processus.
Communication entre Render et Main process
Electron à mit en place un module, appelé IPC, permettant de réaliser une communication ainsi qu’un échange de données entre main et render process, qui est appelable depuis chacun des processus. Cette communication fonctionne sous forme de canaux, et l’échange est bi-latéral. Celle-ci s’apparente à des sockets.
Architecture d’une application Electron
Le schéma suivant montre d’une façon simplifié le fonctionnement de base d’une appli Electron.
Le package.json est le point d’entrée de votre application. Il va indiquer à Electron ou est le main process,
Le main.js définit votre processus principal. Il va créer la fenêtre graphique pour y appeler le render process.
Le index.html définit votre vue.
Le module IPC permet l’échange d’informations entre les divers processus.
Conclusion
Points positifs
Stack web facile à apprendre
Dév rapide ( hot reload, console chronium, modules NPM… )
Cross-platform
Points négatifs
Consommation excessive de RAM
Taille du bundle ( ~100Mo pour un simple ‘Hello World !’ )
Maintenant que vous voyez le fonctionnement global d’un projet sous Electron, je vous propose d’expérimenter vous même, et de réaliser un traitement de texte basique sur le chapitre suivant.
Nous avons vu au chapitre précédent comment créer une frameless window basique.
Nous allons y ajouter quelques fonctionnalités de base que doit avoir un logiciel, via sa barre d’outils.
Le code source concernant ce chapitre est disponible sur mon Github.
Hot reloading du backend
Autant le hot reload des pages web du front se font automatiquement via le module Webpack contenu dans Angular, autant le backend ne s’effectue pas. Pour cela on va ajouter un module npm dans notre projet :
npm install electron-reload
Il va nous permettre de créer un nouveau main process de Electron, en lui donnant simplement en argument le chemin de l’exécutable de electron.
Nous lui ajouterons un argument pour permettre un hard reset du module, ce qui évite d’avoir des processus de Electron fantôme qui peuvent persister.
Et enfin un dernier argument, nous permettant de pouvoir injecter des arguments au lancement de electron, et dans notre cas de garder dans notre environnement de dév, le lancement des dev tools de chronium.
Barre d’outils via l’API de Electron
Définition de la vue, via des flexbox
On va introduire des notions de responsive design qui est propre aux stack du web. Ceci nous permet de rendre adaptable la vue d’une page en fonction de la hauteur et largeur de l’écran de l’utilisateur, et ainsi d’en modifier sa disposition. On parle alors de Flexbox. Celles-ci sont déclaré dans les pages CSS, et permette de définir des règles de disposition entre chaque éléments ( des <div> par exemple ). Cela peut définir des règles pour indiquer comment tel ou tel élément doit grossir, réduire, ou encore se disposer en ligne ou colonne avec ses éléments voisin. Un petit module que j’apprécie et qui est disponible sur NPM, va nous permettre d’induire ces flexbox, directement dans les balises du code HTML de la page :
npm install @angular/flex-layout
On souhaite avoir une barre d’outils comme ceci :
Partie gauche :
Une icone du logiciel avec un bouton d’accueil
Partie du milieu :
Le nom du logiciel
Partie droite :
Une barre d’outils avec des boutons permettant de réduire, de minimiser/maximiser la fenêtre, ainsi qu’un dernier pour fermer la fenêtre
1 – Contener Global
On va commencer par créer un contener global ( notre mat-toolbar ), qui va prendre le maximum d’espace possible de son parent, définit par la directive fxFill :
2 – Création des 3 sous conteners (définit précédemment)
On utilise la directive fxLayout=’row’ afin de créer 3 conteners sur la même ligne. Quand a fxLayoutAlign=’space-between’, elle va nous permettre de définir le type d’espacement entre chacun d’eux. Celle-ci nous permet de les espacer au maximum des un aux autres.
3- Alignement vertical d’un des trois sous conteners
On souhaite qu’ils soient aligné au milieu ce leur ligne. On va donner l’exemple pour le contener de droite. Pour cela, on va ajouter au contener précédent, la directive fxLayout=’column’ pour pouvoir créer des conteners de façon vertical ( rappeler vous que le row permet d’aligner des conteners de façon horizontal), avec le bon fxLayoutAlign=’center’ qui va bien, pour permettre de les aligner au milieu au seins de celui-ci.
Si on reprend notre cheminement depuis le début, on doit normalement avoir un contener fixé à droite de la barre, et qui sera aligné au milieu concernant son axe vertical. On souhaite maintenant avoir y incorporer 3 bouttons d’actions.
4 – Boutons d’actions
On va recréer un contener de type row cette fois-ci, nous permettant de grouper l’ensemble de nos trois boutons de façon horizontal. En effet avec le point précédent, nous étions dans un contener de type column, et donc aligné sur l’axe vertical, chose que l’on ne souhaite pas.
Vous n’avez plus qu’a ajouter vos trois boutons, avec l’appel aux fonctions qui seront déclaré dans le contrôleur, via la directive de angular, (click)=’votre_fonction()’.
Pour un peu d’esthétisme, j’ai rajouté une classeclass=‘button hoverBtnWhite‘, lié dans le fichier CSS, permettant qu’au passage de la souris, la couleur de l’icone et de son background change.
Définition du contrôleur, via le service Electron
Maintenant que la partie graphique est mise en place, on va passer au contrôleur, permettant d’ajouter des actions à nos jolis boutons 😉
On va ajouter un nouveau module, nous permettant d’accéder à l’API de Electron directement depuis notre contrôleur.
npm install ngx-electron --save
On l’importe dans notre module principal, soit App :
import {NgxElectronModule} from 'ngx-electron';
Et on le déclare dans la partie des imports :
import: [ NgxElectronModule ]
Dans notre composant Header.ts, nous aurons besoin d’importer le module ElectronService. En créer un attribut de classe, l’instancier lors de la construction du composant, et se servir du module REMOTE de l’API de Electron. On va alors pouvoir utiliser via ce module, les utilitaires du process main, depuis le render process. Le code suivant vous montre comment lier nos boutons créer précedemment pour leur affecter respectivement les actions suivantes :
fermer la fenêtre,
réduire la fenêtre,
maximiser,
unmaximiser.
Materials icons en offline
Si vous utilisez des icons de la librairie Material, soit celle de base de Angular, vous allez les télécharger à chaque lancement de l’app. Cependant, le jour ou vous voulez déployer votre application hors ligne, plus rien de marche, et les messages d’erreurs ne sont pas tellement explicite, vous êtes obligé d’aller chercher dans les requetes HTTP. Pire si comme moi vous avez du déployer une app offline sur iPad, sans n’avoir de console de développeur de iOS, alors autant prévoir les choses à l’avance. On va utiliser un module disponible sur NPM pour pouvoir toujours les avoir dans notre app :
npm install material-design-icons-iconfont --save
Et ajoutez les lignes suivantes dans votre fichier de style globale de votre app, soit style.scss :
Conclusion
Nous venons de voir comment appeler l’API de Electron depuis notre front en Angular, pour lui ajouter des fonctionnalités simple d’un logiciel.
Nous verrons au prochain chapitre comment créer un explorateur de fichier simple, pour présenter le module ipc de Electron, permettant de communiquer et d’échanger des données entre main et render process.
Je vais vous présenter comme installer votre environnement de développement, et créer votre première fenêtre, avec quelques astuces de dév, vous permettant d’accélérer vos rendus.
Le code source concernant ce chapitre est disponible sur mon Github.
Installation des pré-requis
Vous devrez avoir NodeJS d’installé. Je vous renvoi sur un précédent article qui vous explique les démarches à suivre.
Initialisation d’un nouveau projet
On va initialiser une nouvelle application angular avec tout le squelette de base qui va bien, afin de nous faire gagner du temps :
ng new Nom_de_votre_app
note : ng est une commande de la CLI de Angular
Sélectionner Yes pour avoir le routing du module de base,
Sélectionner ensuite votre langage pour les feuilles de style, je prends SCSS pour ma part.
On va ajouter Electron à notre projet via :
npm install --save-dev electron
Création de la fenêtre principale
On va créer un fichier main.js. C’est lui qui va servir de fichier principal pour créer notre fenêtre, avec le code suivant :
On va ensuite mettre à jour notre fichier package.json pour lui indiquer le point principal d’entrée pour Electron an ajoutant la ligne :
"main": "main.js"
De retour sur la console, déplacer vous au sein du projet, vous allez pouvoir lancer votre application dans electron via la commande :
electron .
Tadaaaaaaaaaaaaa. Ça ne casse pas 3 pattes à un canard, mais ça à le mérite d’être du développement plutôt rapide ! 😂
Liaison du serveur de dév de Angular vers Electron
Nous n’avons fait que lier de façon statique la page index.html. Cependant, pour le bon fonctionnement de Angular, il va nous falloir un serveur HTTP qui gère le typescript. Celui de base de Angular fonctionne très bien.
On va modifier notre fichier main.js pour qu’il prenne non plus un fichier en entrée, mais une URL qui pointe vers notre serveur de développement.
Lancer depuis une console le serveur de développement de Angular :
ng serve
Lancer ensuite depuis une seconde console electron. Vous avez désormais accès à Angular depuis votre application Electron. Vous pouvez avoir accès au rechargement à chaud ( mise à jour de l’UI en direct dès une modification du code ) directement dans Electron.
Lancement parallèle
Pour éviter d’avoir deux console, on va pouvoir automatiser le lancement de Electron et Angular depuis un script.
Dans le fichier package.json, rajoutez les scripts suivants :
La première permet de lancer electron. La seconde quant à elle permet de lancer le serveur local de développement de Angular et Electron de façon concurrentielle.
Customization de la fenêtre
C’est dans l’ère du temps, donnons un peu de style à notre application 😎
On va supprimer la barre d’outils toute moche, et y incorporer une toolbar un poil plus joli, qui nous permettra de déplacer notre app, ainsi que de l’agrandir via une double tap.
Ajoutons la librairie de base pour les material UI :
ng add @angular/material
Nous n’avons pas besoin de HammerJS, mais belle et bien cependant des browser animations pour Angular material, qui seront à préciser à la suite de cette commande.
On va ajouter notre toolbar à notre fichier HTML du composant App
<mat-toolbar class="menu">My App</mat-toolbar>
Ajouter à notre module principal App le bon import pour la librairie Material :
import { MatToolbarModule} from '@angular/material'
Et ajouter MatToolbarModule dans la déclaration du Module principal, dans la partie Import
Lancement instantané de l’app ( white blank screen )
Quand on lance l’application, on a un écran blanc temporaire qui s’affiche. C’est une chose que l’on ne souhaite pas avoir au sein de notre application une fois buildé. C’est pour cela que l’on va modifier notre fichier main.js pour demander à Electron d’afficher notre fenêtre seulement une fois que celle-ci sera entièrement chargé, ce qui nous donnera l’impression d’avoir une ouverture quasi instantané.
Pour cela on va créer notre fenêtre et demander à Electron de la cacher dans un premier temps. Ajoutez la ligne suivante en paramètre de création lors de l’appel de la méthode :
show: false
On va ensuite rajouter un event, qui sera appelé une fois que la fenêtre sera prête :
Notre application va désormais se lancer directement.
Icone de l’application
Pour changer l’icone de votre application sur le bureau de windows, ajouter l’option lors de la création de la fenêtre avec le lien pointant vers une image placé dans votre dossier d’assets :
icon: './src/assets/icon/icon_transparent.png'
Conclusion
Nous venons de créer très simplement et rapidement une simple fenêtre, avec un chouette esthétisme.
Le prochain chapitre va permettre d’y ajouter de nouvelles fonctionnalités, concernant la barre d’outils.