Créer un champ personnalisé dans l'espace d'administration depuis un plugin

Dans ce tutoriel, nous allons créer le code d'un champ et l'insérer dans l'espace d'administration d'un site web. En prérequis, il faut avoir déjà créé un plugin comme indiqué dans le tutoriel de création de plugin. Avant de commencer, vous devez donc avoir un site ParoiCMS avec un plugin fork-or-knife-plugin.

Définir un nouveau champ

Dans le dossier du plugin (fork-or-knife-plugin), créez un sous-dossier site-schema-lib. Il contiendra deux fichiers.

Le fichier field-lib.site-schema.json décrit le champ :

{
  "version": "3.1",
  "languages": ["fr"],
  "fieldTypes": [
    {
      "name": "forkOrKnife",
      "storedOn": "leaf",
      "storedAs": "varchar",
      "dataType": "string",
      "plugin": "fork-or-knife-plugin"
    }
  ]
}

Nous venons de déclarer un champ dont la clé est forkOrKnife. Sa valeur est partagée par les éventuelles traductions du document ("storedOn": "leaf") et il est stocké sous forme de chaîne de caractères. Il utilise notre plugin (c'est un effet de la propriété "plugin": "fork-or-knife-plugin"), et cela implique de lui donner un comportement spécial. Les sections suivantes de ce document auront pour objectif d'implémenter ce comportement.

À lire aussi, la documentation sur les champs.

Le deuxième fichier est nommé field-lib.site-schema.l10n.fr.json et contient le libellé du champ en français à afficher dans l'espace d'administration :

{
  "fieldTypes": {
    "forkOrKnife": {
      "label": "Fourchette ou couteau ?"
    }
  }
}

Pour que notre morceau de site-schema soit pris en compte, il faut référencer le dossier site-schema-lib afin de le rendre disponible sur ParoiCMS :

  1. Éditer le fichier backend/src/plugin.ts
  2. À l'intérieur de la fonction siteInit, ajoutez :
service.registerSiteSchemaLibrary(join(packageDir, "site-schema-lib"));

Créer une application SolidJS qui s'exécutera dans l'espace d'administration

Modifier le fichier package.json du plugin

Éditez le fichier package.json du plugin (celui situé dans le dossier fork-or-knife-plugin). Dans la section des "scripts", ajoutez :

"build:bo": "(cd bo-front && tsc && vite build)",
"build:bo:watch": "(cd bo-front && tsc && vite build --watch)"

Modifiez également la commande "build" dans cette même section afin d'y ajouter && npm run build:bo. Votre commande "build" ressemblera à ceci :

"build": "npm run build:backend && npm run build:bo"

Dans la section "devDependencies", ajoutez les dépendances suivantes :

    "@paroicms/public-bo-lib": "0.12.0",
    "sass": "~1.77.8",
    "solid-js": "~1.8.20",
    "vite": "~5.4.0",
    "vite-plugin-solid": "~2.10.2"

NB : N'hésitez pas à utiliser les versions les plus récentes de ces dépendances.

Nous venons d'ajouter les dépendances et les commandes de build d'une application SolidJS située dans le plugin. Il nous reste à créer cette application, ce qui est l'objet de la section suivante.

Créer une application SolidJS

Dans le dossier du plugin, créez un sous-dossier bo-front. Les fichiers que nous allons y placer sont ceux d'une application SolidJS classique.

Dans le dossier bo-front, créez un fichier tsconfig.json :

{
  "compilerOptions": {
    "strict": true,
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "jsx": "preserve",
    "jsxImportSource": "solid-js",
    "types": [
      "vite/client"
    ],
    "noEmit": true,
    "isolatedModules": true,
  },
}

Toujours dans le dossier bo-front, créez un fichier vite.config.ts :

import { resolve } from "node:path";
import { defineConfig } from "vite";
import solidPlugin from "vite-plugin-solid";

export default defineConfig({
  plugins: [
    solidPlugin(),
  ],
  build: {
    target: "esnext",
    outDir: "dist",
    lib: {
      entry: resolve(__dirname, "src/main.tsx"),
      fileName: () => "bo-plugin.mjs",
      formats: ["es"],
    },
    rollupOptions: {
      output: {
        format: "es",
        inlineDynamicImports: false,
      },
    },
  },
});

Créez un sous-dossier bo-front/src/ qui contiendra les trois fichiers suivants :

// main.tsx

import "./styles.scss"

import type {
  BoPlugin,
  BoPluginInitOptions,
  BoPluginInstance,
  BoPluginService,
} from "@paroicms/public-bo-lib";
import { createEffect, createRoot, createSignal } from "solid-js";
import ForkOrKnifeField from "./ForkOrKnifeField";

const plugin: BoPlugin<string> = {
  create,
  init,
};

export default plugin;

function init({ pluginBaseUrl }: BoPluginInitOptions) {
  const cssUrl = `${pluginBaseUrl}/style.css`;
  const link = document.createElement("link");
  link.rel = "stylesheet";
  link.href = cssUrl;
  document.head.appendChild(link);
}

function create(service: BoPluginService<string>): BoPluginInstance<string> {
  if (service.fieldType.dataType !== "string") {
    throw new Error(`data type '${service.fieldType.dataType}' is incompatible, should be 'string'`);
  }

  return createRoot((dispose) => {
    const [value, setValue] = createSignal(service.value);
    const [language, setLanguage] = createSignal(service.language);

    createEffect(() => {
      service.onChange(value());
    });

    return {
      element: (
        <ForkOrKnifeField
          language={language}
          value={value}
          setValue={setValue}
        />
      ) as HTMLElement,
      setLanguage,
      setValue: (list) => {
        setValue(list);
      },
      getValue: value,
      dispose,
    };
  });
}

C'est le point d'entrée de l'application SolidJS. Ce fichier exporte deux fonctions décrites par le type BoPlugin<string> dans le package @paroicms/public-bo-lib :

Il faut également appeler service.onChange chaque fois que la valeur du champ change. Cet appel n'est pas optionnel : le back-office utilise ce mécanisme pour récupérer la valeur modifiée.

// ForkOrKnifeField.tsx

import type { Accessor, Setter } from "solid-js";

export interface ForkOrKnifeFieldProps {
  language: Accessor<string>;
  value: Accessor<string | undefined>;
  setValue: Setter<string | undefined>;
}

export default function ForkOrKnifeField(props: ForkOrKnifeFieldProps) {
  const { value, setValue } = props;

  return (
    <form class="FkField">
      <label>
        <input
          type="radio"
          name="utensil"
          value="fork"
          checked={value() === "fork"}
          onChange={() => setValue("fork")}
        />
        ⑂
      </label>
      <label>
        <input
          type="radio"
          name="utensil"
          value="knife"
          checked={value() === "knife"}
          onChange={() => setValue("knife")}
        />
        🔪
      </label>
    </form>
  );
}


// styles.scss

.FkField {
  display: flex;
  gap: 10px;

  label {
    align-items: center;
    border: 2px solid #ccc;
    border-radius: 10px;
    cursor: pointer;
    display: flex;
    font-size: 2rem;
    height: 100px;
    justify-content: center;
    transition: background-color 0.3s, border-color 0.3s;
    width: 100px;
  }

  input[type="radio"] {
    display: none;
  }

  label:has(input[type="radio"]:checked) {
    background-color: #007bff;
    border-color: #007bff;
    color: white;
  }
}

Voir aussi : le site officiel de SolidJS.

Une chose à savoir : il n'est pas obligé d'écrire cette application en SolidJS. L'application d'un champ est intégrée par ParoiCMS en tant que module JavaScript standard, et peu importe le framework avec lequel elle est faite.

À ce niveau, vous êtes prêt à compiler notre application SolidJS. Dans un terminal, placez-vous dans le dossier du plugin, puis exécutez les commandes suivantes :

# installer les nouvelles dépendances
npm i

# compiler
npm run build:bo

Si la compilation s'est bien passée, alors un dossier bo-front/dist a été créé et les fichiers bundlés pour le CSS et pour le javascript sont déposés dedans.

Servir les fichiers de l'application SolidJS

Pour que l'application SolidJS soit prise en compte, il faut référencer le dossier bo-front/dist afin de la rendre disponible depuis le back-office :

  1. Éditez le fichier backend/src/plugin.ts.
  2. À l'intérieur de la fonction siteInit, ajoutez :
    service.setBoAssetsDirectory(join(packageDir, "bo-front", "dist"));

Puisque nous avons modifié le code du backend, il faut recompiler. Dans le dossier du plugin, exécutez la commande :

npm run build

Le plugin est prêt.

Ajouter notre nouveau champ dans le site

Il est temps de sortir du dossier du plugin. Dans le fichier site-schema.json du site web, au niveau de la description du type de document pour la page d'accueil, c'est-à-dire sous la ligne "typeName": "home", insérez :

      "fields": [
        "forkOrKnife"
      ],

À ce niveau, nous devrions être capables d'utiliser notre nouveau composant dans l'espace d'administration :

  1. Lancez le serveur du site si ce n'est pas déjà fait (npm run dev).
  2. Rafraîchissez l'espace d'administration dans le navigateur.
  3. Éditez la page d'accueil. Vous devriez voir apparaître notre nouveau champ :

Il nous reste à l'utiliser dans la partie publique du site.

Utiliser le nouveau champ dans un template Liquid

Éditez le fichier theme/templates/home.liquid. La première chose à faire est d'inspecter la payload afin de vérifier si notre champ s'y trouve. Pour cela, ajoutez quelque part :

{{ doc | info }}

La payload est imposante, mais quelque part dedans nous pouvons voir :

Voici un exemple de code Liquid qui utilise notre nouveau champ :

    <p>Je mange avec
      {% if doc.field.forkOrKnife == "fork" %}
        une fourchette
      {% elsif doc.field.forkOrKnife == "knife" %}
        un couteau
      {% else %}
        les doigts
      {% endif %}
    </p>

… Cela affichera quelque chose comme :

Je mange avec une fourchette

Dans ce tutoriel, nous avons déclaré un nouveau type de champ. Nous avons implémenté une application JavaScript pour éditer ce champ depuis l'espace d'administration, et nous avons utilisé le résultat stocké dans le champ pour le rendu de la page web correspondante.