Bonjour !
Je suis développeuse sur le produit Approbiom (porté par le ministère de l’agriculture).
Nous utilisons Grist et nous voulons faire des vues personnalisées avec la couche DSFR.
Je me demandais si vous aviez déjà créer des widgets Grist et si vous aviez des pistes de réflexions pour les sujets suivants :
Comment mockez-vous les données de Grist pendant le développement de vos widgets ?
Comment vous testez vos widgets ? Avez-vous des tests de bout en bout ?
Comment vous structurez votre projet ?
Comment vous gérez les environnements de préproduction, staging et production pour les documents Grist ?
J’ai déjà pu commencer à réfléchir à ces sujets mais je suis preneuse de partager nos expériences
Je me permet de taguer ici @celine , @Amandine et @Antoine.R au cas où ils ont des retours à partager. Mais je suis sûr qu’il y a d’autres membres de la communauté pourraient partager d’autres choses intéressantes !
Comme mentionner par @enro, j’avais documenté le déploiement du projet GitHub - gristlabs/grist-widget: A repository of custom widgets to embed in Grist documents · GitHub, dans un environnement Windows pour faciliter la conception de widget Grist, afin de pouvoir tester en livereload son widget dans un conteneur Grist local, mais j’avais trouvé que le déploiement était un peu “long”, complexe et clairement pas assez user-friendly.
Et en discutant avec d’autres collègues développeurs, j’ai imaginé une autre piste plus efficace,
Ce projet grist-custom-vite s’inscrit (comme son nom l’indique) dans l’écosystème Vite.js, et pourrait vous aider à structurer vos projets, en vous inspirant des bonnes pratiques de dev web (notamment pour les problématiques de gestion de multiples environement, de tests, de qualité, de build… et finalement pour architecturer vos projets avec une convention partagée…) et en plus de bénéficier d’un outil en ligne de commande pour déployer rapidement un environnement de développement local.
Avertissement : Ce projet est actuellement en phase de conception (développement finalisé à environ 75 %). Il s’agit d’une version expérimentale non définitive, sujette à des modifications majeures.
Caractéristiques principales :
Environnement clé en main (Zero-Setup) : Un espace de travail entièrement orchestré et prêt à l’emploi pour les développeurs, comprenant une instance Grist locale conteneurisée.
Plusieurs variantes (Flavors) : Démarrez votre projet avec votre pile technique préférée, prenant en charge à la fois Vanilla et React, en JavaScript ou en TypeScript.
Bundling flexible : Choisissez votre stratégie de déploiement grâce au support du découpage standard des ressources (Standard asset splitting) avec gestion du cache, ou d’un build SPA (Single Page Application).
Publication automatisée du manifest Grist widget dans le conteneur local Docker Grist-core. Lors du lancement de l’environement de dev locale, le manifest des Widgets Grist (dans lequel se trouve également celui que vous etre en train de développer) est automatiquement publié et reliée à l’instance Docker Grist-Core déployée.
CLI Devtools unifié : Gérez sans effort le déploiement d’un environnement de développement local, la compilation de production et la prévisualisation grâce à une application CLI unique offrant des commandes simples (dev, build, preview).
Exemple : Démarrer l’environnement de développement Grist local avec pnpm dev
— Lancement de l’environnement de développement local
Ce projet ne répondra pas directement à vos problématiques, mais vous donnera un squelette (boilerplate) pour démarrer un projet Grist custom widget dans Vite.js, qui lui vous répondra.
Pour l’instant le projet propose 4 “flavors” de template. L’idée à terme est d’implémenter les mêmes que les officielles du projet Vite.js.
React en JavaScript
React en TypeScript avec React Compiler
Vanilla en JavaScript
Vanilla en TypeScript
Et sinon pour répondre à vos questions plus précisément… qui ne sont pas en lien direct avec ce projet, qq élements de réponses
Comment vous structurez votre projet ?
Vous l’aurez compris, que j’utilise l’écosystème Vite.js, avec une préférence pour le framework React en TypeScript avec les particularités suivantes.
React Router Dom pour la gestion du routing (lorsque j’ai besoin que le widget serve plus d’une seule page). Dans ce cas, j’utilise son HashRouter au niveau du composant de l’application React, avec une unique méthode qui centralise la définition des routes de l’application (cf. HashRouter | React Router)
React Router Hash Link si j’ai besoin d’un routage par ancre au sein d’une page. Ce dernier permet de gérer le chargement de page, puis de faire défiler automatiquement jusqu’à un élément ciblé (une ancre HTML) une fois que le DOM est chargé.
Sécurité et qualité du code avec TypeScript. En associant le langage TypeScript (surcouche de JavaScript) dans le développement de widget, on apporte un typage strict au coeur des composants React, gage de qualité/sécurité dans le monde “trop” libre du dev JavaScript. TypeScript permet de détecter les erreurs dans l’IDE (avant l’exécution), et fludifie le DX (dev xperience) avec une autocomplétion avancée… bref contrainte de coût à l’entrée pour l’apprentissage (quoi que avec l’ia ^^…), mais énorme gain à la sortie !
React Compiler. Offre lors de la compilation (transpilation du code lors du build) une analyse du code à la volée pour injecter automatiquement des optimisations, là où elles sont nécessaires (cf. React Compiler – React )
Pour l’organisation des environnements, j’utilise la convention .env dans Vite.js (comme celle qu’on peut trouver dans l’écosysème Symfony). A la racine du projet j’ajoute 3 fichiers
.env <= configuration commune
.env.production <= spécifique à la prod
.env.developement <= spécifique au dev
Par exemple
# ─────────────────────────────────────────────
# Application
# ─────────────────────────────────────────────
VITE_SPA_TITLE="Mon widget"
# ─────────────────────────────────────────────
# Open Graph metadata
# ─────────────────────────────────────────────
# Title displayed when sharing the SPA
VITE_SPA_OG_TITLE=""
# Description used by social platforms when the SPA is shared
VITE_SPA_OG_DESCRIPTION=""
# Path to the default OG image displayed when sharing the SPA
VITE_SPA_OG_IMAGE="/og-image.png"
# Canonical URL used for link previews
VITE_SPA_OG_URL="https://your-app.example.com"
# Name of the site or application
VITE_SPA_OG_SITE_NAME="Your App Name"
# Content type for Open Graph (e.g., website, article)
VITE_SPA_OG_TYPE="website"
# Locale used for OG metadata (e.g., en_US, fr_FR)
VITE_SPA_OG_LOCALE="fr_FR"
# ─────────────────────────────────────────────
# Data Source Configuration
# ─────────────────────────────────────────────
# Options: "mock-json", "mock-sqlite" or "grist"
VITE_PORTAL_DATA_SOURCE="grist"
# Grist access mode: "read table" or "full"
VITE_GRIST_ACCESS_MODE="read table"
Aprés il suffit de surcharger la conf pour le dev dans .env.developemnt
# ─────────────────────────────────────────────
# Environment DEVELOPMENT
# ─────────────────────────────────────────────
VITE_PORTAL_DATA_SOURCE="mock-sqlite"
VITE_GRIST_ACCESS_MODE="full"
Ces variables d’environnement préfixées par VITE_ sont injectées lors de la phase HMR en dev ou de build et sont accessibles par :
Injection statique (Template HTML). Vite permet de remplacer dynamiquement des variables directement dans index.html via la syntaxe
Accès dynamique (Javascript). Dans le code applicatif, l’ensemble des variables est exposé via l’objet global import.meta.env. Cet objet est disponible dans tous vos composants et modules
const title = `Accueil – ${import.meta.env.VITE_SPA_OG_TITLE}`;
if (import.meta.env.DEV) {
console.log("Configuration :", import.meta.env.VITE_SPA_APP_TITLE);
}
Pour les tests unitaires/intégration, j’utilise la solution Vitest.js (https://vitest.dev/) qui s’intègre facilement à un projet Vite… il est fait pour ca, et permet de faire entre autre du BDD (behaviour driven dev). Mais j’aime bien également l’approche playground pour « tester » en live des composants react. Je me suis préparé une solution home-made minimanislite pour faire du playground/storybook local (je peux vous partager le code si vous le souhaitez) ou sinon vous pouvez utilisez le standard avec la librairie Storybook => Intro to Storybook | Storybook Tutorials
Pour le mocking j’ai une première approche home-made dans un service React que je détaille ci-dessous. En effet lorsque l’on développe un widget pour Grist, on est dépendant du contexte d’exécution. Si votre code appelle directement l’API window.grist au chargement, l’application plantera ou tournera à vide lorsqu’elle est lancée sur votre machine locale (hors de l’iFrame Grist). Une solution est d’exécuter votre application dans une véritable instance de Grist (ce que permet de faire le projet grist-widget-vite dans un contenur local Grist), mais pour le dev quotidien il est plus intéressant d’utiliser un mocking des données.
Pour résoudre ce problème, je vous partage l’architecture que j’ai mise en place avec un DataProvider polymorphe basé sur le pattern Factory. Il permet de basculer de manière transparente entre de vraies données Grist et des mockups locaux (fichiers JSON ou base SQLite réelle) via une simple variable d’environnement Vite (VITE_PORTAL_DATA_SOURCE).
Mode
Source
Type de connexion
Description
grist
API Distante
Réseau (HTTP)
En production en tant que custom widget GRIST
mock-sqlite
Fichier .grist (base SQLITE)
Locale (SQLite)
Ultra-rapide, idéal pour les tests.
mock-json
Fichiers JSON statiques
Locale (File)
Mode de secours & prototype rapide.
Voir le code source du service
Explication TL;DR ^^..
Coeur du service : DataProvider.ts
Pour que le reste de l’application (composants React, hooks, etc.) n’ait pas à se soucier d’où proviennent les données, on définit un point d’entrée unique. La Factory instancie le bon fournisseur en fonction de la configuration d’environnement. Cela permet de maintenir un code métier « pur », totalement découplé des spécificités de l’API Grist ou du système de fichiers local.
Data driver pour la production GristDataProvider.ts
En production lorsque le widget est embarqué dans une réelle instance de Grist, ce DataProvider interroge l’API de Grist (docApi.fetchtable) pour récupérer les données réelles du document.
Attention au p’tit piège de Grist, la méthode window.grist.docApi.fetchTable retourne les données dans un format colonnaire (ex: { id: [1, 2], name: ["A", "B"] }). C’est performant pour Grist, mais contre-intuitif en dev où on préfère itérer sur des lignes (objets). GristDataProvier nettoie le jeux de données avec columnarToRows.
Data driver pour le dev avec mocking JSON statique MockJSONDataProvider.ts
Idéal pour des structures simples et un prototypage rapide en tulisant un simple fichier JSON pour simuler les données d’une table de Grist. Si le widget demande la table Grist Utilisateurs, le provider effectue un import dynamique du fichier ./MockData/Utilisateurs.json. A charge pour le développeur d’écrire son jeu de données dans le fichier au format JSON.
Data driver pour le dev avec mockint SQLite MockSQLiteDataProvider.ts
Un fichier .grist n’est rien d’autre qu’une base de données SQLite. En utilisant la librairie sql.js (compilé en WebAssembly), le data provider charge directement votre fichier de base Grist local dans le navigateur, liste les tables disponibles et joue de vraies requêtes SQL (SELECT * FROM ...) pour extraire les données mockées de votre document Grist. Les données retournées imitent parfaitement la structure de lignes attendue par le widget, c’est le document Grist lui même qui est exploitée par ce data driver. A charge pour le développeur d’exporter/créer un document spécifique pour l’env de développement.
Pour fonctionner, il suffit de déposer un export Document.grist dans ./MockSQLite/ pour disposer d’un environnement de test identique à la production, sans aucune connexion réseau.
Intégration React application
Au démarrage de l’application REACT, on n’appelle window.grist.ready()que si on est explicitement en mode de production Grist, évitant ainsi de bloquer l’application lorsque cette dernière n’est pas utilisé dans une instance Grist.
const App = () => {
if (import.meta.env.DEV) {
import("./assets/styles/playground.css");
}
useEffect(() => {
// Initialize Grist API only if datasource is set to 'grist' and Grist API is available
const dataSource = import.meta.env.VITE_PORTAL_DATA_SOURCE;
const accessMode = import.meta.env.VITE_GRIST_ACCESS_MODE || 'read table';
console.info('[App] Initialized with data source:', dataSource);
if (dataSource === 'grist' && window.grist) {
if (typeof window.grist === "undefined" || !window.grist?.docApi?.fetchTable) {
throw new Error(`Grist plugin API is not available.`);
}
window.grist.ready(accessMode);
console.info('[App] Grist API initialized with access mode:', accessMode);
}
}, []);
return (
<>
<HashRouter>
<Header title={import.meta.env.VITE_SPA_TITLE} subtitle={import.meta.env.VITE_SPA_SUBTITLE}/>
<main className="fr-container fr-mt-6w fr-mb-10w">
<AppRouter/>
</main>
<Footer/>
</HashRouter>
</>
);
}
Consommation transparente via un Hook (useGristTables.ts)
Dans vos views, il suffit d’utiliser le hook react useGristTable qui se charge de paralléliser les appels (via Promise.all) au bon data driver. Ce hook appelle dataProvider.fetchTable sans jamais savoir si les données viennent de Grist, d’un JSON ou de SQLite :
// Exemple d'utilisation dans un composant
const { data, loading, error } = useGristTables({
students: "Liste_Eleves",
schools: "Etablissements"
});
// data.students contiendra les lignes de la table students, peu importe la source
// data.schoolscontiendra les lignes de la table schooles, peu importe la source
Agnostique et simplicité d’utilisation ^^…
Pour le building, un dernier point pour le build de l’application j’utilise le plugin vite-plugin-singlefile lorsque je veux faire une SPA (single page application). Je suis en train de finaliser la commande de build dans le projet Grist widget vite afin de simplifier son utilisation, mais en attendant voila la conf dans Vite.js pour l’implémenter.
// ./vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { viteSingleFile } from "vite-plugin-singlefile";
/**
* The "vite-plugin-singlefile" is a vite build plugin, taht allows you to inline all JavaScript and CSS resources
* directly into the final dist/index.html file. By doing this, your entire web app can be embedded and distributed
* as a single HTML file.
**/
export default defineConfig({
plugins: [
react(),
viteSingleFile()
],
build: {
// Transpiles your modern JavaScript/TypeScript down to ES2018 syntax, ensuring compatibility with older browsers that don't support newer ECMAScript features
target: "es2018",
// Bundles all CSS into a single file instead of splitting it across multiple files
cssCodeSplit: false,
// Use esbuild (Vite's default bundler) to compress and minify your code, reducing bundle size for faster downloads
minify: "esbuild",
// Disables source maps in production
sourcemap: false,
// Set the output directory for the build
outDir: "build",
}
});
En conclusion, quelque soit le framework web que vous aimez utiliser (ou pas de framework = Vanilla), je ne peux que vous inviter à utiliser l’écosystème Vite.js pour vous aider dans les problématiques que vous avez soulever…et plus encore (HMR à la place du livereload…)
Grist et nous voulons faire des vues personnalisées avec la couche DSFR.
J’ai plusieurs solutions à vous partager
Vous installer en tant que dépendances le DSFR dans votre projet (npm install, yarn add, pnpm add …), et vous vous charger de son impémentation. (CSS, Js)
Vous utilisez un framework, dans ce cas pourquoi ne pas utiliser des portages existants pour smiplifier l’utilsiation des composants du DSFR. Cette approche offre moins de liberté que sa propre implémentation, mais est largement plus confortable à utiliser.
Cette semaine, j’ai dû alimenter un dashboard composé de plusieurs tables d’indicateurs qui ne dispose pas encore de données réelles. Je pense que vous serez aussi intéressé par cette problématique du mock-data (génération data fictives).
Bonjour, Quelqu’un sait-il si une homologation SSI est actuellement en cours ou déjà validée pour l’utilisation des Custom Widgets Grist et de l’API grist-plugin-api.js ? Je suis sur l’instance Grist de la DINUM et je cherche simplement un retour d’expérience ou un contact ayant déjà traité ce sujet. Merci.