Nous allons maintenant créer un composant React et le rendre disponible à l’éditeur. Nous allons supposer que nous voulons créer un Compteur dans cet exemple.

Étapes de création

1

Créer un composant React

Dans src/webparts/monSuperSite/components/, créer un dossier Counter avec un fichier Counter.tsx

import { useState } from 'react';

const Counter = () => {
// Declare a state variable 'count' and a function to update it.
const [count, setCount] = useState(0);

return (
    <div style={{ textAlign: 'center', marginTop: '20px' }}>
    <h1>Counter: {count}</h1>
    <button onClick={() => setCount(count + 1)}>Increase</button>
    <button onClick={() => setCount(count - 1)} style={{ marginLeft: '10px' }}>Decrease</button>
    <button onClick={() => setCount(0)} style={{ marginLeft: '10px' }}>Reset</button>
    </div>
);
};

export default Counter;
La récupération de données dynamiques doit se faire au niveau du composant React à l’aide de la hook useEffect par exemple.
2

Créer le plugin du composant GrapeJS

Dans src/webparts/monSuperSite/grapejs/components/, créer un dossier Counter avec un fichier Counter.tsx Il faut faire en sorte que le nom du type du composant soit unique.

    import * as ReactDOM from "react-dom";
    import Counter from "../../../components/Counter/Counter";
    import { Editor } from "grapesjs";

    export const CounterComponent = (editor: Editor) => {
        editor.DomComponents.addType('counter-type', {
            // Model definition
            model: {
                // Default properties
                defaults: {
                    tagName: 'div',
                    draggable: true,
                    droppable: false, // Indique si on peut injecter d'autre composants dedans
                    traits: [ // 
                        {
                            type: 'text', // Types Built-in, voir la documentation pour plus d'informations
                            label: '...',
                            name: 'test', // This binds the 'url' attribute to the UI and makes it editable
                            changeProp: true, // This flag makes it react to changes,
                            valueTrue: "true", // Value to assign when is checked, default: `true`
                            valueFalse: "false",
                        },
                        // ...
                    ]
                },
                updated(property: string, value: any, prevValue: any) {
                    // Si le composant dépend des attributs définis par l'utilisateur,
                    // il faut mettre à jour le composant à chaque modification (en fonction des attributs)
                    ReactDOM.hydrate(<Counter />, this.view.el);
                },
            },
            view: {
                // This is where you mount the React component
                onRender({ el }: { el: HTMLElement }) {
                    ReactDOM.render(<Counter />, el);
                },
            },
        });
    }

Il est possible aussi de récupérer des données externes à cette étape via la méthode de lifecycle init. Néanmoins, cela fait en sorte que même les données sont enregistrés avec les données du projet, et ainsi le composant n’est dynamique que lorsqu’on le glisse dans le Canvas. Au moment du rendu des pages, le contenu sera statique.

Liens utiles:
https://grapesjs.com/docs/modules/Components.html#how-components-work

3

Regrouper dans un plugin global

Dans src/webparts/monSuperSite/grapejs/components/index.ts :

    import { CounterComponent } from "./Counter/Counter";


    export const ComponentsPlugin = [
        CounterComponent,
        ...
    ];
4

Enregistrer le plugin dans l'éditeur GrapeJS

Dans src/webparts/monSuperSite/grapejs/index.tsx :

    // ...
    useEffect(() => {
        editorRef.current = grapesjs.init({
            // ...
            plugins: [...ComponentsPlugin],
        })
    }, []);
    // ...

Chaque composants doit être défini comme un plugin.

5

Enregistrer dans le Block Manager

Dans src/webparts/monSuperSite/grapejs/index.tsx :

    // ...
    useEffect(() => {
        editorRef.current = grapesjs.init({
            // ...
            blockManager : {
                // ...
                blocks: [
                    // ...
                    {
                        id: "counter", // Unique
                        label: "Counter",
                        select: true,
                        content: { type: 'counter-type' }, // Type du composant : unique
                        activate: true,
                        category: "Custom Blocks" // Optionnel : Catégorie du block pour l'organisation
                    },
                ]
            }
        })
    }, []);
    // ...
6

Gérer le rendu des pages

Cette étape est à faire une seule fois. Elle permet de gérer le rendu des pages à partir des données de GrapeJS.

Dans src/webparts/monSuperSite/components/, créer un dossier Page avec un fichier Page.tsx : Ce composant gère l’affichage des pages.

    import { ReactChildren } from "react";
    import { Link } from "react-router-dom";
    import Wrapper from "../Wrapper/Wrapper";

    const Page = ({children, gjsPage, styles} : {children ?: ReactChildren, gjsPage: any, styles : any})=>{

        // Fonction qui permet d'afficher les composants de manière récursive afin de gérer les composants enfants
        const renderComponents = (component)=>{
            let count = 0; // Key pour mapper les composants
            return (
                <Wrapper key={count++} type={component.type} component={component} styles={styles}>
                    {component.components && component.components.map(renderComponents)}
                </Wrapper>
            );
        }

        return (
            <div>
                {renderComponents(gjsPage.frames[0].component)}
            </div>
        );
    }

    export default Page;

Dans src/webparts/monSuperSite/components/, créer un dossier Wrapper avec un fichier Wrapper.tsx : Ce composant gère l’affichage de tous les composants.

    import { ReactChildren } from "react";
    import Counter from "../Counter/Counter";
    import Users from "../Users/Users";
    import Form from "../Form/Form";
    import FormListing from "../FormListing/FormListing";

    const Wrapper = ({type, component, children, styles}: {type: string, component: any, children: ReactChildren, styles: any,}) => {
        
        // Fonction qui permet de récupérer les styles des composants
        const componentStyles = styles.find(elem=>{
            return elem?.selectors.includes(`#${component?.attributes?.id}`);
        })?.style || {};

        // Une fonction qui permet de récupérer les classes est envisageable

        const renderComponent = ()=>{
            switch(type){
                case 'text': // Gérer les composants Built-in aussi
                    if(component.tagName){
                        const CustomTag = component.tagName;
                        return <CustomTag style={componentStyles}>{children}</CustomTag>
                    }
                    return <p style={componentStyles}>{children}</p>
                case 'textnode':
                    return <>{component.content}</>
                case 'counter-type':
                    return <Counter />
                default:
                    return <>{children}</>;
            }
        }
        
        return (
            <>
                {renderComponent()}
            </>
        );
    }

    export default Wrapper;

Exemple : Récupération des données à partir de Sharepoint

Ci dessous est un exemple d’un composant qui récupère des données à partir d’une liste de Sharepoint :

1

Créer un composant React

Dans src/webparts/monSuperSite/components/, créer un dossier FormListing avec un fichier FormListing.tsx

    import { SPFI } from "@pnp/sp";
    import { getSP } from "../../pnpjsConfig";
    import "@pnp/sp/webs";
    import "@pnp/sp/lists";
    import "@pnp/sp/items";
    import "@pnp/sp/items/list";
    import { useCallback, useEffect, useState } from "react";
    import "../../../../styles/dist/tailwind.css";



    export default function FormListing({ form_name }: { form_name: string }) {
    const sp: SPFI = getSP();
    const [data, setData] = useState<any>([]);

    const fetchData = useCallback(async () => {
        try{
            const items = await sp?.web.lists.getByTitle(form_name).items();
            setData(items);
        }catch(err){
            setData([]);
            console.log(err);
        }
    }, [form_name, sp]);
    useEffect(() => {
        if (sp) {
            void fetchData();
        }
    }, [fetchData]);

    return (
        <>
        <h1>Listing</h1>
        <div className="grid grid-cols-3 gap-4 mx-8 my-4">
            {data && data?.map((elem : any) => {
            return (
                <div key={elem.ID} className="overflow-hidden rounded-lg shadow transition hover:shadow-lg">
                <div className="bg-white p-4 sm:p-6">
                    <h3 className="mt-0.5 text-lg text-gray-900">{elem.nom} {elem.prenom}</h3>

                    <p className="mt-2 line-clamp-3 text-sm/relaxed text-gray-500">
                    {elem.description}
                    </p>
                </div>
                </div>
            );
            })}
        </div>
        </>
    );
    }
2

Créer le plugin du composant GrapeJS

Dans src/webparts/monSuperSite/grapejs/components/, créer un dossier FormListing avec un fichier FormListing.tsx Il faut faire en sorte que le nom du type du composant soit unique.

    import { Editor } from "grapesjs";
    import * as ReactDOM from "react-dom";
    import "@pnp/sp/webs";
    import "@pnp/sp/lists";
    import "@pnp/sp/views/list";
    import { getSP } from "../../../pnpjsConfig";
    import FormListing from "../../../components/FormListing/FormListing";

    export const FormListingComponent = (editor : Editor)=>{
        const sp = getSP();

        editor.DomComponents.addType('form-listing-type', {
            model : {
                defaults: {
                    tagName: 'div',
                    draggable: true,
                    droppable: false,
                    prop_form_name: "",
                    traits: [
                        {
                            type: 'select',
                            label: 'Nom de la liste du formulaire',
                            name: 'prop_form_name', // This binds the 'url' attribute to the UI and makes it editable
                            changeProp: true, // This flag makes it react to changes,
                            options: []
                        }
                    ]
                },
                async init(){
                    const trait = this.getTrait("prop_form_name");
                    const lists = (await sp.web.lists()).filter(elem=>elem.Title.startsWith("Formulaire-"));
                    trait.set({
                        options: lists.map(elem=>elem.Title),
                    });
                },
                updated(property: string, value: any, prevValue: any) {
                    if(property == "traits"){
                        ReactDOM.hydrate(<FormListing form_name={this.get("prop_form_name")}/>, this.view.el);
                    }
                },
            },
            view : {
                onRender({ el, model }: { el: HTMLElement, model: any }) {
                    ReactDOM.render(<FormListing form_name={model.get("prop_form_name")}/>, el);
                },
            }
        })
    }
3

Regrouper dans un plugin global

Dans src/webparts/monSuperSite/grapejs/components/index.ts :

    import { FormListingComponent } from "./FormListing/FormListing";


    export const ComponentsPlugin = [
        FormListingComponent,
        ...
    ];
4

Enregistrer le plugin dans l'éditeur GrapeJS

Dans src/webparts/monSuperSite/grapejs/index.tsx :

    // ...
    useEffect(() => {
        editorRef.current = grapesjs.init({
            // ...
            plugins: [...ComponentsPlugin],
        })
    }, []);
    // ...

Chaque composants doit être défini comme un plugin.

5

Enregistrer dans le Block Manager

Dans src/webparts/monSuperSite/grapejs/index.tsx :

    // ...
    useEffect(() => {
        editorRef.current = grapesjs.init({
            // ...
            blockManager : {
                // ...
                blocks: [
                    // ...
                    {
                        id: "form-listing",
                        label: "Form listing",
                        select: true,
                        content: { type: 'form-listing-type' },
                        activate: true,
                        category: "Custom Blocks"
                    },
                ]
            }
        })
    }, []);
    // ...
6

Gérer le rendu des pages

Cette étape est à faire une seule fois. Elle permet de gérer le rendu des pages à partir des données de GrapeJS.

Dans src/webparts/monSuperSite/components/, créer un dossier Page avec un fichier Page.tsx : Ce composant gère l’affichage des pages.

    import { ReactChildren } from "react";
    import { Link } from "react-router-dom";
    import Wrapper from "../Wrapper/Wrapper";

    const Page = ({children, gjsPage, styles} : {children ?: ReactChildren, gjsPage: any, styles : any})=>{

        // Fonction qui permet d'afficher les composants de manière récursive afin de gérer les composants enfants
        const renderComponents = (component)=>{
            let count = 0; // Key pour mapper les composants
            return (
                <Wrapper key={count++} type={component.type} component={component} styles={styles}>
                    {component.components && component.components.map(renderComponents)}
                </Wrapper>
            );
        }

        return (
            <div>
                {renderComponents(gjsPage.frames[0].component)}
            </div>
        );
    }

    export default Page;

Dans src/webparts/monSuperSite/components/, créer un dossier Wrapper avec un fichier Wrapper.tsx : Ce composant gère l’affichage de tous les composants.

    // ...
    import FormListing from "../FormListing/FormListing";

    const Wrapper = ({type, component, children, styles}: {type: string, component: any, children: ReactChildren, styles: any,}) => {

        const renderComponent = ()=>{
            switch(type){
                // ...
                case 'form-listing-type':
                    return <FormListing form_name={component.prop_form_name}/>
                // ...
            }
        }
        // ...
    }

    export default Wrapper;